[英]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.