[英]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
存根文件是包含类型提示的文件,仅供类型检查器使用,而不是在运行时使用。
使用存根文件.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 来让 mypy 平静下来,该实现将被稍后创建的符号覆盖。
def foobar_A1(foo: int) -> str:
c = i
return ""
foobar_A1 = inject(foobar, ServiceA1)
.pyi
首先存在的原因:没有运行时的注释。问题未解决?试试以下方法:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.