簡體   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 的風險。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM