繁体   English   中英

Partial apply后如何精确标注function

[英]How to make precise function annotation after Partial applied

给定一个 function:

def foobar(foo: int, bar: str, spam: SpamService) -> str:
    return spam.serve(foo, bar)

这个 function,在外观上类似于 FastAPI 端点,将两个参数定义为“正常”参数,一个“服务”,一个抽象 class。我想“重用” foobar function,就像我在路由器中重用 FastAPI 端点一样,并注册n function 给定n依赖项的“版本”。

例子:

foobar_rabbit = inject(foobar, RabbitService)
foobar_snake = inject(foobar, SnakeService)
foobar_rabbit(1, "rabot")
foobar_snake(2, "sniky")

我可以使用functools.partial来做到这一点,但我希望在不依赖 position 或关键字参数的情况下将依赖项作为正确的参数注入。

这意味着 function 需要两个依赖项,例如:

def foobar(foo: int, egg: EggService, spam: SpamService) -> str:
    return spam.serve(foo, egg.do_stuff())

可以这样注册:

foobar_1 = inject(foobar, SpamService1, EggService2)
foobar_1_ = inject(foobar, EggService2, SpamService1)  # result in the same Partial

为此,我编写了这段代码(应按原样在 python 3.11 上运行,无外部依赖):

import abc
import functools
import inspect
import typing


class Service(abc.ABC):
    ...


class ServiceA(Service):
    @staticmethod
    @abc.abstractmethod
    def method_a(a: int) -> str:
        """
        This method do something.
        """


class ServiceA1(ServiceA):
    @staticmethod
    def method_a(a: int) -> str:
        return f"A1: {a}"


def inject(
        func: typing.Callable,
        *services: typing.Type[Service]
) -> functools.partial:
    annotations = inspect.get_annotations(func)
    del annotations["return"]

    bind_services = {
        key: service
        for key, value in annotations.items()
        if issubclass(value, Service)
        for service in services
        if issubclass(service, value)
    }

    return functools.partial(func, **bind_services)


def foobar(foo: int, spam: ServiceA) -> str:
    return spam.method_a(foo)


foobar_A1 = inject(foobar, ServiceA1)

if __name__ == '__main__':
    print(foobar_A1(1))  # A1: 1

问题是foobar_A1的签名。 如果我不发送任何 arguments,Pycharm 将不会发出警告,mypy 也不会发现任何错误。

例如,我尝试了很多使用typing.TypeVar替代方法,但没有任何效果。

这是一个无效解决方案的示例:

_SERVICE = typing.TypeVar("_SERVICE", bound=Service)
_RETURN = typing.TypeVar("_RETURN")

def inject(
        func: typing.Callable[[..., _SERVICE], _RETURN],
        *services: typing.Type[Service]
) -> functools.partial[typing.Callable[[_SERVICE, ...], _RETURN]]:

但是 mypy 抱怨说它没有创建预期的签名(我还不习惯这种注释魔法)。

预期签名: (foo: int) -> str

这不能用inject()注释来完成,但我们仍然可以为foobar_A1符号创建适当的注释。

使用存根文件.pyi

PEP 484 – 类型提示#存根文件

存根文件是包含类型提示的文件,仅供类型检查器使用,而不是在运行时使用。

使用存根文件.pyi允许显式定义符号的签名,而无需在主.py文件中进行任何复杂的注释。

基于问题代码片段的示例:

# .pyi
def foobar_A1(foo: int) -> str: ...

我的结果

如果在没有任何参数的情况下使用foobar_A1()

file.py:line: error: Missing positional argument "foo" in call to "foobar_A1"  [call-arg]

为什么这个解决方案很棒?

用例是创建一个 SDK 即 python package 供外部使用。 这是.pyi的典型用例,甚至是使用此类文件的好习惯。 这不是技巧、解决方法、复杂或难以阅读。 我认为这是一个非常 pythonic 的解决方案。

选择

创建一个虚拟 function

您可以通过创建一个带有伪造实现的虚拟 function 来让 mypy 平静下来,该实现将被稍后创建的符号覆盖。

def foobar_A1(foo: int) -> str:
    c = i
    return ""

foobar_A1 = inject(foobar, ServiceA1)

为什么我拒绝这种选择

  1. 即使它适用于 mypy,pycharm 仍然会抱怨重新声明的符号没有使用。
  2. 它在运行时创建一个临时的 function 仅供类型检查器使用。 这就是.pyi首先存在的原因:没有运行时的注释。
  3. 迫使您考虑满足 mypy 的虚拟实现,并适应每个 function 注入。 如果实际符号被错误重命名或删除,样板代码有暴露虚拟 function 的风险。

问题未解决?试试以下方法:

Partial apply后如何精确标注function

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2023 STACKOOM.COM