[英]Defered invocation of a python classmethod obtained in a decorator
我有一個裝飾器用來包裝這樣的類方法:
class Class(object):
@register_classmethod
@classmethod
def my_class_method(cls):
...
我的裝飾器得到一個classmethod
當我嘗試調用它時,它拋出class method is not callable
。
這是一個示例,其中包含一個過於簡化的裝飾器實現:
from typing import Callable
all_methods: list[Callable[[type], None]] = []
def register_classmethod(classmeth: Callable[[type], None]) -> Callable[[type], None]:
all_methods.append(classmeth)
return classmeth
class Class(object):
@register_classmethod
@classmethod
def my_class_method(cls) -> None:
print(f"Hello from {cls}.my_class_method")
@classmethod
def run_registered_classmethods(cls) -> None:
for classmeth in all_methods:
classmeth(cls)
Class.run_registered_classmethods()
雖然mypy --strict
對輸入非常滿意,但在執行時我得到:
$ python3 testscripts/test-classmethod-call.py
Traceback (most recent call last):
File ".../test-classmethod-call.py", line 20, in <module>
Class.run_registered_classmethods()
File ".../test-classmethod-call.py", line 18, in run_registered_classmethods
classmeth(cls)
TypeError: 'classmethod' object is not callable
現在,我確實在重構@classmethod
上沒有顯式my_class_method
的代碼,並且該代碼運行良好:
$ python3 testscripts/test-classmethod-call.py
Hello from <class '__main__.Class'>.my_class_method
然而,通過上面的類型注釋, mypy
盡職盡責地指出我們正在嘗試在此處注冊一個實例方法:
testscripts/test-classmethod-call.py:10: error: Argument 1 to "register_classmethod" has incompatible type "Callable[[Class], None]"; expected "Callable[[type], None]" [arg-type]
注意:我認為這也是python 如果我只有 object 如何調用類方法所面臨的問題,但它的初始表述可能不夠准確。
看起來我們在此上下文中得到的是 class 方法下的描述符 object。 我認為我們需要將它綁定到我們的包裝器描述符中,例如。 從 3.11 開始使用MethodType
,如此處 所示:
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if hasattr(type(self.f), '__get__'):
# This code path was added in Python 3.9
# and was deprecated in Python 3.11.
return self.f.__get__(cls, cls)
return MethodType(self.f, cls)
但是我們不能將類方法 object 傳遞給MethodType
,並且必須在其(未記錄的 AFAICT) __func__
成員中挖掘它。
現在這樣做了:
@classmethod
def run_registered_classmethods(cls) -> None:
for classmeth in all_methods:
bound_method = types.MethodType(classmeth.__func__, cls)
bound_method()
然而,這讓我們回到了一個新的類型問題: classmethod
修飾的方法具有classmethod
類型,但被注釋為Callable
以便用戶程序理解它,這導致mypy
抱怨:
testscripts/test-classmethod-call.py:19: error: "Callable[[type], None]" has no attribute "__func__" [attr-defined]
我們可以通過assert(isinstance(...))
的方式教他關於真實類型的知識,並最終得到有效且類型良好的代碼:
@classmethod
def run_registered_classmethods(cls) -> None:
for classmeth in all_methods:
assert isinstance(classmeth, classmethod)
bound_method = types.MethodType(classmeth.__func__, cls)
bound_method()
這可行,但assert
確實有運行時成本。 所以我們想以更好的方式給出提示,例如使用typing.cast()
:
@classmethod
def run_registered_classmethods(cls) -> None:
for classmeth in all_methods:
bound_method = types.MethodType(cast(classmethod, classmeth).__func__, cls)
bound_method()
但是,如果mypy
表面上對此感到滿意,那么使用它的--strict
選項表明我們的輸入並不像它應該的那樣精確:
testscripts/test-classmethod-call.py:20: error: Missing type parameters for generic type "classmethod" [type-arg]
所以classmethod
是泛型類型? 很確定我在文檔中沒有找到任何提示。 幸運的是, reveal_type()
和一些直覺似乎暗示泛型類型參數是 class 方法的返回類型:
testscripts/test-classmethod-call.py:21: note: Revealed type is "def [_R_co] (def (*Any, **Any) -> _R_co`1) -> builtins.classmethod[_R_co`1]"
(是的,哎喲! )
但是,如果cast(classmethod[None], classmeth)
對mypy
讀取 OK,則python
本身不太高興: TypeError: 'type' object is not subscriptable
。
所以我們還必須讓解釋器和類型檢查器使用typing.TYPE_CHECKING
來查看不同的代碼,這給我們帶來了以下內容:
import types
from typing import Callable, cast, TYPE_CHECKING
all_methods: list[Callable[[type], None]] = []
def register_classmethod(classmeth: Callable[[type], None]) -> Callable[[type], None]:
all_methods.append(classmeth)
return classmeth
class Class(object):
@register_classmethod
@classmethod
def my_class_method(cls) -> None:
pass
@classmethod
def run_registered_classmethods(cls) -> None:
for classmeth in all_methods:
if TYPE_CHECKING:
realclassmethod = cast(classmethod[None], classmeth)
else:
realclassmethod = classmeth
bound_method = types.MethodType(realclassmethod.__func__, cls)
bound_method()
Class.run_registered_classmethods()
...傳遞為:
$ mypy --strict testscripts/test-classmethod-call.py
Success: no issues found in 1 source file
$ python3 testscripts/test-classmethod-call.py
Hello from <class '__main__.Class'>.my_class_method
對於我們想要簡單和可讀的東西來說,這似乎過於復雜,並且可能會提供像下面這樣的通用幫助器來使所有這些更有用——我對它不是很滿意,即使它通過了所有以上測試:
_ClassType = TypeVar("_ClassType")
_RType = TypeVar("_RType")
def bound_class_method(classmeth: Callable[[type[_ClassType]], _RType],
cls: type[_ClassType]) -> Callable[[], _RType]:
if TYPE_CHECKING:
realclassmethod = cast(classmethod[None], classmeth)
else:
realclassmethod = classmeth
return types.MethodType(realclassmethod.__func__, cls)
它不處理任意 arguments 到 class 方法,我們可能會繞過使用ParamSpec 。 但這仍然使用了類__func__
classmethod
泛型類型參數):文檔沒有提到它們。
這些答案中有大量信息,謝謝:)
我發現其中最有用的是:
@classmethod
裝飾的方法得到它的第一個參數cls
type[Class]
而不是Class
(@droooze) 接受要裝飾的方法沒有 class 方法簽名這一事實; 讓裝飾器在用classmethod
包裝方法之前注冊方法,以避免處理后者的內部結構,然后返回包裝后的版本 (@chepner)。
請注意,當我們需要在我們的類方法中使用cls
作為類型時,我們可以這樣做:
@register_classmethod def my_class_method(cls) -> None: klass = cast(type[Class], cls) print(f"Hello from {klass}.my_class_method")
遺憾的是 class 名稱必須被硬編碼,並且如果我們想比Callable[[Any], None]
做得更好,則必須進一步修改register_classmethod
參數的類型注釋
忍受顯式添加@classmethod
並使用它的內部結構,這也可能很煩人,因為強制使用該附加裝飾器會導致 API 更改 (@droooze)
告訴類型檢查器我們的裝飾器是特殊的,比如classmethod
,它的第一個參數是type[Class]
,否則它會被注釋為Class
。 缺點是(除了編寫和維護插件的成本),每個 static 檢查器都需要一個單獨的插件。
Class 方法不可調用; 他們定義了一個__get__
方法,該方法返回一個可調用method
實例,該實例將 class 作為第一個參數傳遞給基礎 function。
我可能會讓register_classmethod
既存儲function又返回一個 class 方法:
all_methods: list[classmethod] = []
def register_classmethod(classmeth: classmethod) -> classmethod:
all_methods.append(classmeth)
return classmethod(classmeth)
class Class(object):
@register_classmethod
def my_class_method(cls) -> None:
print(f"Hello from {cls}.my_class_method")
@classmethod
def run_registered_classmethods(cls) -> None:
for classmeth in all_methods:
classmeth(cls)
這樣, run_registered_classmethods
不需要擔心描述符協議:它只是直接運行底層 function。
內置裝飾器@property
、 @classmethod
和@staticmethod
可能是 4 種主要類型檢查器實現中的每一個的特例,這意味着與其他裝飾器的交互可能沒有任何意義,即使您理論上有類型注釋正確。
對於mypy
, @classmethod
是特殊情況,因此
collections.abc.Callable
,即使classmethod
甚至沒有__call__
方法;type[<owning class>]
。 事實上,如果你用 builtins.classmethod 裝飾它,你甚至可以獲得第一個參數type[<owning class>]
builtins.classmethod
的唯一方法; 任何類型構造的其他自定義實現都不會起作用,即使是在主體中沒有實現的classmethod
的直接子類也是如此。正如您所發現的,這就是運行時錯誤的原因。
如果你專門使用mypy
,在你給出的例子中,我會像這樣調整它:
from __future__ import annotations
import collections.abc as cx
import typing as t
clsT = t.TypeVar("clsT", bound=type)
P = t.ParamSpec("P")
R_co = t.TypeVar("R_co", covariant=True)
all_methods: list[classmethod[t.Any]] = []
def register_classmethod(classmeth: cx.Callable[[clsT], R_co]) -> classmethod[R_co]:
# The assertion performs type-narrowing; see
# https://mypy.readthedocs.io/en/stable/type_narrowing.html
assert isinstance(classmeth, classmethod)
all_methods.append(classmeth) # type: ignore[unreachable]
return classmeth
class Class(object):
@register_classmethod
@classmethod
def my_class_method(cls) -> None:
print(f"Hello from {cls}.my_class_method")
# Not a callable!
my_class_method() # mypy: "classmethod[None]" not callable [operator]
@classmethod
def run_registered_classmethods(cls) -> None:
for classmeth in all_methods:
classmeth.__func__(cls)
# Too many arguments
@register_classmethod # mypy: Argument 1 to "register_classmethod" has incompatible type "Callable[[Type[Class], int], None]"; expected "Callable[[type], None]" [arg-type]
@classmethod
def bad_too_many_args(cls, a: int) -> None:
return
Class.run_registered_classmethods()
如果你對classmethod
做更多的事情並且需要在所有范圍內進行適當的類型檢查,我會重新實現classmethod
的類型,如下所示:
from __future__ import annotations
import collections.abc as cx
import typing as t
# This is strictly unnecessary, but demonstrates a more accurately implemented
# `classmethod`. Accessing this from inside the class body, from an instance, or from
# a class works as expected.
# Unfortunately, you cannot use `ClassMethod` as a decorator and expect
# the first parameter to be typed correctly (see explanation 2.)
if t.TYPE_CHECKING:
import sys
clsT = t.TypeVar("clsT", bound=type)
P = t.ParamSpec("P")
R_co = t.TypeVar("R_co", covariant=True)
class ClassMethod(t.Generic[clsT, P, R_co]):
# Largely re-implemented from typeshed stubs; see
# https://github.com/python/typeshed/blob/d2d706f9d8b1a568ff9ba1acf81ef8f6a6b99b12/stdlib/builtins.pyi#L128-L139
@property
def __func__(self) -> cx.Callable[t.Concatenate[clsT, P], R_co]: ...
@property
def __isabstractmethod__(self) -> bool: ...
def __new__(cls, __f: cx.Callable[t.Concatenate[clsT, P], R_co]) -> ClassMethod[clsT, P, R_co]:...
def __get__(self, __obj: t.Any, __type: type) -> cx.Callable[P, R_co]: ...
if sys.version_info >= (3, 10):
__name__: str
__qualname__: str
@property
def __wrapped__(self) -> cx.Callable[t.Concatenate[clsT, P], R_co]: ... # Same as `__func__`
else:
ClassMethod = classmethod
all_methods: list[ClassMethod[type, [], t.Any]] = []
def register_classmethod(
classmeth: cx.Callable[[clsT], R_co]
) -> ClassMethod[clsT, [], R_co]:
# The assertion performs type-narrowing; see
# https://mypy.readthedocs.io/en/stable/type_narrowing.html
assert isinstance(classmeth, ClassMethod)
all_methods.append(classmeth) # type: ignore[unreachable]
return classmeth
class Class(object):
@register_classmethod
@classmethod
def my_class_method(cls) -> None:
print(f"Hello from {cls}.my_class_method")
# Not a callable! Fixes problem given in explanation 1.
my_class_method() # mypy: "ClassMethod[Type[Class], [], None]" not callable [operator]
@classmethod
def run_registered_classmethods(cls) -> None:
for classmeth in all_methods:
classmeth.__func__(cls)
# Not enough arguments
classmeth.__func__() # mypy: Too few arguments [call-arg]
# Too many arguments - `typing.ParamSpec` is working correctly
@register_classmethod # mypy: Argument 1 to "register_classmethod" has incompatible type "Callable[[Type[Class], int], None]"; expected "Callable[[type], None]" [arg-type]
@classmethod
def bad_too_many_args(cls, a: int) -> None:
return
Class.run_registered_classmethods()
# `__get__` working correctly - on the descriptor protocol.
# These two error out both for static type checking and runtime.
Class.my_class_method(type) # mypy: Too few arguments [call-arg]
Class.my_class_method.__func__ # mypy: "Callable[[], None]" has no attribute "__func__" [attr-defined]
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.