簡體   English   中英

如何為 static 屬性提供類型支持(使用裝飾器)

[英]How to have typing support for a static property (using a decorator)

給定一個 static 屬性裝飾器:

class static_property:
    def __init__(self, getter):
        self.__getter = getter

    def __get__(self, obj, objtype):
        return self.__getter(objtype)

    @staticmethod
    def __call__(getter_fn):
        return static_property(getter_fn)

這適用於 class,如下所示:

class Foo:
    @static_prop
    def bar(self) -> int:
        return 10

添加呼叫為 static:

>>> print(Foo.bar)
10

我將如何向Foo.bar static_property推斷為類型int而不是Any

還是有另一種方法來創建裝飾器以支持類型推斷?

另請參閱:如何在 python 中定義類字段,即類的實例

長話短說

我的首選解決方案:(使用 Python 3.9 - 3.11測試)

from __future__ import annotations
from collections.abc import Callable
from typing import Generic, TypeVar


R = TypeVar("R")


class static_property(Generic[R]):
    def __init__(self, getter: Callable[[], R]) -> None:
        self.__getter = getter

    def __get__(self, obj: object, objtype: type) -> R:
        return self.__getter()

    @staticmethod
    def __call__(getter_fn: Callable[[], R]) -> static_property[R]:
        return static_property(getter_fn)


class Foo:
    @static_property
    def bar() -> int:  # type: ignore[misc]
        return 10

詳情見第 3 節)。


0) 問題:裝修到底是什么?

當前編寫代碼的方式表明您想要創建一個描述符,您可以用它來裝飾class方法,正如您將 objtype 傳遞給objtype中的 getter __get__這一事實所證明的那樣。

另一方面,描述符的名稱 class static_propertybar實際上是 static (對實例或類不做任何事情)這一事實表明您實際上想用它裝飾static方法。 這將需要一種非常不同的方法。


1) 采用class方法的通用描述符

但首先,以class作為唯一參數的方法的解決方案。

這可以通過使static_property描述符 class 根據被修飾方法的返回類型R通用(在以下示例中為intstr )來實現。

完整的工作示例

from collections.abc import Callable
from typing import Any, Generic, TypeVar


R = TypeVar("R")


class static_property(Generic[R]):
    def __init__(self, getter: Callable[[Any], R]) -> None:
        self.__getter = getter

    def __get__(self, obj: object, objtype: type) -> R:
        return self.__getter(objtype)

    @staticmethod
    def __call__(getter_fn: Callable[[Any], R]) -> "static_property[R]":
        return static_property(getter_fn)


class Foo:
    @static_property
    def bar(cls) -> int:
        return 10

    @static_property
    def baz(cls) -> str:
        return "a"


x = Foo().bar
y = Foo.baz
reveal_type(x)
reveal_type(y)

通過mypy --strict運行它會得到以下 output:

note: Revealed type is "builtins.int"
note: Revealed type is "builtins.str"
Success: no issues found in 1 source file

警告

從技術上講,我們已經遇到了一個問題,因為barbaz方法仍然被類型檢查器視為常規實例方法,這意味着它期望第一個參數是Foo實例,而不是 class 本身。 這就是為什么我在描述符方法中使用Any作為Callable的參數。

這在這種情況下並不真正相關,因為(正如我上面提到的)這些方法實際上是 static,所以第一個參數是沒有意義的。 更重要的是,由於barbaz方法僅在描述符的__get__中以未綁定的形式使用,因此注釋在技術上仍然是正確的。


2) 采用static方法的通用描述符

假設您實際上希望static_property修飾 static 方法,這意味着在這種情況下方法實際上采用任何 arguments,這將需要不同的方法。

由於內置的staticmethod class 是Callable的子類型(由於它顯然支持__call__協議),我們可以用staticmethod修飾barbaz方法並將結果對象傳遞給我們自己的static_property.__call__

我們可以保留R ,因為typeshed也根據傳遞給它的方法的返回類型將staticmethod定義為泛型。 (至少對於 Python 3.10+ 。)這意味着我們可以保留有關barbaz方法的返回類型的信息,即使我們用staticmethod修飾它們也是如此。

完整的工作示例

from collections.abc import Callable
from typing import Generic, TypeVar


R = TypeVar("R")


class static_property(Generic[R]):
    def __init__(self, getter: Callable[[], R]) -> None:
        self.__getter = getter

    def __get__(self, obj: object, objtype: type) -> R:
        return self.__getter()

    @staticmethod
    def __call__(getter_fn: Callable[[], R]) -> "static_property[R]":
        return static_property(getter_fn)

class Foo:
    @static_property
    @staticmethod
    def bar() -> int:
        return 10

    @static_property
    @staticmethod
    def baz() -> str:
        return "a"


x = Foo().bar
y = Foo.baz
reveal_type(x)
reveal_type(y)

mypy --strict output 仍然與上面的相同,這意味着類型被正確地推斷為intstr

如您所見,getter function 現在可以簡單地注釋為Callable[[], R] ,即一個不采用 arguments 的可調用函數,但返回我們的描述符的類型參數。


3)擺脫staticmethod方法

最后一步是完全不需要使用staticmethod裝飾器。 不幸的是,這不能通過對其進行子類化並再次根據R將我們的子類顯式定義為泛型來實現。 這主要是由於staticmethod.__get__返回一個可調用對象(再次參見 typeshed 以供參考),但我們明確希望它返回R

那么阻礙我們的是mypy (可能還有其他類型檢查器)期望每個沒有用staticmethod修飾的方法至少接受一個參數。 因此,如果我們將barbaz定義為不占用 arguments ,我們會收到投訴。

到目前為止,除了明確忽略該雜項錯誤並按如下方式進行之外,我沒有找到解決此問題的方法。

工作示例

from __future__ import annotations
from collections.abc import Callable
from typing import Generic, TypeVar


R = TypeVar("R")


class static_property(Generic[R]):
    def __init__(self, getter: Callable[[], R]) -> None:
        self.__getter = getter

    def __get__(self, obj: object, objtype: type) -> R:
        return self.__getter()

    @staticmethod
    def __call__(getter_fn: Callable[[], R]) -> static_property[R]:
        return static_property(getter_fn)


class Foo:
    @static_property
    def bar() -> int:  # type: ignore[misc]
        return 10

    @static_property
    def baz() -> str:  # type: ignore[misc]
        return "a"


x = Foo().bar
y = Foo.baz
reveal_type(x)
reveal_type(y)

明顯的警告

顯然再次正確推斷出類型,但是mypy會在沒有type: ignore指令的情況下報錯,說error: Method must have at least one argument 描述符仍然可以正常工作,這里沒有實際的類型安全問題。 在這里要明確一點, type: ignore是因為barbaz的簽名中缺少參數mypy通常認為這是不安全的。

實際上是類型安全的原因是我們再次只處理未綁定的方法barbaz 裝飾器“消耗”它們,這意味着它們永遠不會被調用綁定到它們的 class 或其實例。 因此,我們的方法不需要任何 arguments。

(順便說一句,我只是在這里使用__future__.annotations來避免圍繞static_property[R]的引號。你顯然可以對上面的解決方案 1) 和 2) 做同樣的事情。)

到處取舍

最終,您似乎必須決定哪種解決方案對您最有用。 每個都有自己的缺點。 看起來我們現在不能以 100% 干凈的方式做到這一點,但也許其他人會找到一種方法,或者打字系統可能會改變方式以允許更好的解決方案。

暫無
暫無

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

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