我想要一个Python装饰器:
mypy
我在这里找到了回答大多数组合的答案,但不是全部四个。
这是我最接近的一次。 Python 3.8.
#!/usr/bin/env python3.8
from __future__ import annotations
from typing import (
Callable,
TypeVar,
Generic,
Optional,
Any,
Type,
cast,
overload,
)
from typing_extensions import Concatenate, ParamSpec, reveal_type
T_o = TypeVar("T_o") # Type of the class instance
T_ret = TypeVar("T_ret") # Return type of the decorated function
P = ParamSpec("P") # Param spec for decorated unbound function arguments
class MyDecoCls(Generic[T_o, T_ret, P]):
@overload # Decorator with args: __init__(self, arg1=..., arg2=...)
def __init__(self, /, arg1: bool = False, arg2: bool = False) -> None: ...
@overload # Decorator without args: __init__(self, funct)
def __init__(
self, funct: Callable[Concatenate[T_o, P], T_ret], /
) -> None: ...
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.with_args = not (args and callable(args[0]))
print(f"In __init__({self}, {args}, {kwargs})")
self.funct: Optional[Callable[Concatenate[T_o, P], T_ret]] = None
self.arg1 = kwargs.get("arg1", False)
self.arg2 = kwargs.get("arg2", False)
if not self.with_args:
self.funct = cast(Callable[Concatenate[T_o, P], T_ret], args[0])
def __get__(self, instance: T_o, owner: Type[T_o], /) -> Callable[P, T_ret]:
# Decorator without args: __get__(self, obj, type(obj)) -> wrapper
print(f"In __get__({self}, {instance}, {owner})")
assert instance
assert self.funct
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T_ret:
assert self.funct
ret = self.funct(instance, *args, **kwargs) # Use instance here
print(f"In wrapper({args}, {kwargs}) -> {ret} (__get__)")
print(f" self.funct: {self.funct}; instance: {instance}")
return ret
reveal_type(wrapper)
# Explicitly cast the wrapper to the correct type
return cast(Callable[P, T_ret], wrapper)
reveal_type(__get__)
def __call__(
self, funct: Callable[Concatenate[T_o, P], T_ret], /
) -> Callable[Concatenate[T_o, P], T_ret]:
# Decorator with args: __call__(self, funct) -> wrapper
print(f"In __call__({self}, {funct})")
assert self.with_args
assert callable(funct)
self.funct = funct
def wrapper(self: T_o, *args: P.args, **kwargs: P.kwargs) -> T_ret:
ret = funct(self, *args, **kwargs)
print(f"In wrapper({self}, {args}, {kwargs}) -> {ret} (__call__)")
return ret
reveal_type(wrapper)
return cast(Callable[Concatenate[T_o, P], T_ret], wrapper)
reveal_type(__call__)
def __str__(self) -> str:
args = "with args" if self.with_args else "without args"
return f"<MyDecoCls object ({args}) {hex(id(self))[-6:]}>"
# Example usage
class Foo:
@MyDecoCls
def bar(self, i: int) -> int:
print(f"{self}.bar({i})")
return i * 2
reveal_type(bar)
@MyDecoCls(arg1=True)
def baz(self, /, i: int) -> int:
print(f"{self}.baz(i={i})")
return i * 4
reveal_type(baz)
def __str__(self) -> str:
return "<Foo object>"
# Test the implementation using the example usage
f = Foo()
res: int
print(f"object f = {f}")
print(f"running bar: {(res := f.bar(42))}")
assert res == 42 * 2, f"res was {res}"
print(f"running baz: {(res := f.baz(i=13))}")
assert res == 13 * 4, f"res was {res} != 13 * 4"
脚本运行正确。
Runtime type is 'function'
Runtime type is 'function'
In __init__(<MyDecoCls object (without args) 4da340>, (<function Foo.bar at 0x7f0dab2abf70>,), {})
Runtime type is 'MyDecoCls'
In __init__(<MyDecoCls object (with args) 43d160>, (), {'arg1': True})
In __call__(<MyDecoCls object (with args) 43d160>, <function Foo.baz at 0x7f0dab2a5040>)
Runtime type is 'function'
Runtime type is 'function'
object f = <Foo object>
In __get__(<MyDecoCls object (without args) 4da340>, <Foo object>, <class '__main__.Foo'>)
Runtime type is 'function'
<Foo object>.bar(42)
In wrapper((42,), {}) -> 84 (__get__)
self.funct: <function Foo.bar at 0x7f0dab2abf70>; instance: <Foo object>
running bar: 84
<Foo object>.baz(i=13)
In wrapper(<Foo object>, (), {'i': 13}) -> 52 (__call__)
running baz: 52
但是
mypy --strict
失败了。
foo.py:52: note: Revealed type is "def (*P.args, **P.kwargs) -> T_ret`2"
foo.py:57: note: Revealed type is "def (foo.MyDecoCls[T_o`1, T_ret`2, P`3], T_o`1, Type[T_o`1]) -> def (*P.args, **P.kwargs) -> T_ret`2"
foo.py:73: note: Revealed type is "def (self: T_o`1, *P.args, **P.kwargs) -> T_ret`2"
foo.py:77: note: Revealed type is "def (foo.MyDecoCls[T_o`1, T_ret`2, P`3], def (T_o`1, *P.args, **P.kwargs) -> T_ret`2) -> def (T_o`1, *P.args, **P.kwargs) -> T_ret`2"
foo.py:91: note: Revealed type is "foo.MyDecoCls[foo.Foo, builtins.int, [i: builtins.int]]"
foo.py:93: error: Argument 1 to "__call__" of "MyDecoCls" has incompatible type "Callable[[Foo, int], int]"; expected "Callable[[Never, VarArg(Never), KwArg(Never)], Never]" [arg-type]
foo.py:98: note: Revealed type is "def (Never, *Never, **Never) -> Never"
foo.py:110: error: Invalid self argument "Foo" to attribute function "baz" with type "Callable[[Never, VarArg(Never), KwArg(Never)], Never]" [misc]
foo.py:110: error: Argument "i" to "baz" of "Foo" has incompatible type "int"; expected "Never" [arg-type]
Found 3 errors in 1 file (checked 1 source file)
问题出在带有参数的装饰器中。 如何才能使其发挥作用?
这是一个令人惊讶的好问题,并不是关于 Python 静态类型中不可能的事情。
基本上,每当你声明一个泛型类时,只有两种方法来解决所涉及的泛型变量。第一个是常见的:您根据这些变量定义一个构造函数(
__new__
或__init__
,是的,它们并不是真正的“构造函数”,但让我们将讨论留给语言律师),并且类型检查器可以解决它们。第二种不太常见,但在野外仍然遇到:您可以通过类型化赋值或参数化类(x: defaultdict[str, int] = defaultdict(int)
)显式提供这些参数(想象一下x = defaultdict[str, int](int)
- 没有注释就无法推断关键类型)。无论哪种方式,您都需要在实例化类时了解all泛型变量(或者,最坏的情况是实例化并立即分配给类型化变量)。没有办法说“请从稍后的调用中推断它” - 这是因为“下一个调用”的定义通常是不明确的,特别是如果多个方法依赖于此变量。
现在,在这种情况下,第二个几乎无法使用:您必须使用基本上重复的方法签名进行注释。所以首选第一种方式(像往常一样)。
当
funct
直接传递给构造函数时效果很好 - 有足够的信息来解决所有 P
、T_o
和 T_ret
。但是,第一个 (with-args) 重载不涉及任何这些变量。在这种情况下,类型检查器只能推断出诸如pyright的“未知”之类的内容,并且(通常)发出诸如“需要类型注释......”之类的警告。然而,无法在那里添加注释,因此 mypy
只是默默地退回到 Never
推理,而没有早期诊断。当您执行 MyDecoCls(arg1=True)
时,它具有类型 MyDecoCls[Never, Never, Never]
- 不是您可以使用任何函数 __call__
的类型。
现在我们知道问题是什么了,但是如何解决呢?您的
__call__
方法仅适用于 with-args 场景,因此它不关心类的变量 - 在这种情况下,它们总是 1 被解析为 Never
。让我们忘记那些类绑定变量并引入新的三元组,仅绑定到 __call__
方法。
让我们也摆脱
cast
函数的那些荒谬的 wrapper
:第一个即使在您的代码片段中也是完全不必要的,而另一个只需要使 self
posonly。
# These three parametrize no-args version
T_o = TypeVar("T_o") # Type of the class instance
T_ret = TypeVar("T_ret") # Return type of the decorated function
P = ParamSpec("P") # Param spec for decorated unbound function arguments
# These three only apply to with-args case
T_o2 = TypeVar("T_o2")
T_ret2 = TypeVar("T_ret2")
P2 = ParamSpec("P2")
class MyDecoCls(Generic[T_o, T_ret, P]):
@overload # Decorator with args: __init__(self, arg1=..., arg2=...)
def __init__(self, /, arg1: bool = False, arg2: bool = False) -> None: ...
@overload # Decorator without args: __init__(self, funct)
def __init__(
self, funct: Callable[Concatenate[T_o, P], T_ret], /
) -> None: ...
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.with_args = not (args and callable(args[0]))
print(f"In __init__({self}, {args}, {kwargs})")
self.funct: Optional[Callable[Concatenate[T_o, P], T_ret]] = None
self.arg1 = kwargs.get("arg1", False)
self.arg2 = kwargs.get("arg2", False)
if not self.with_args:
self.funct = cast(Callable[Concatenate[T_o, P], T_ret], args[0])
def __get__(self, instance: T_o, owner: Type[T_o], /) -> Callable[P, T_ret]:
# Decorator without args: __get__(self, obj, type(obj)) -> wrapper
print(f"In __get__({self}, {instance}, {owner})")
assert instance
assert self.funct
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T_ret:
assert self.funct
ret = self.funct(instance, *args, **kwargs) # Use instance here
print(f"In wrapper({args}, {kwargs}) -> {ret} (__get__)")
print(f" self.funct: {self.funct}; instance: {instance}")
return ret
reveal_type(wrapper)
return wrapper
reveal_type(__get__)
def __call__(
self, funct: Callable[Concatenate[T_o2, P2], T_ret2], /
) -> Callable[Concatenate[T_o2, P2], T_ret2]:
# Decorator with args: __call__(self, funct) -> wrapper
print(f"In __call__({self}, {funct})")
assert self.with_args
assert callable(funct)
# See note [1] regarding safety of this
self.funct = funct # type: ignore[assignment]
def wrapper(self: T_o2, /, *args: P2.args, **kwargs: P2.kwargs) -> T_ret2:
ret = funct(self, *args, **kwargs)
print(f"In wrapper({self}, {args}, {kwargs}) -> {ret} (__call__)")
return ret
reveal_type(wrapper)
return wrapper
reveal_type(__call__)
这现在可以很好地验证您的示例(playground)。
但是,在这种情况下,使用函数装饰器可以更直观。即使您的类实际上要大得多,它仍然可以重构为一组函数。如果你这样做,就不会出现类似的范围问题。这是我自己解决这个问题的方法(playground,通过mypy --strict
):
T_o = TypeVar("T_o") # Type of the class instance
T_ret = TypeVar("T_ret") # Return type of the decorated function
P = ParamSpec("P") # Param spec for decorated unbound function arguments
Fn: TypeAlias = Callable[Concatenate[T_o, P], T_ret]
@overload
def my_deco(fn: Fn[T_o, P, T_ret], /) -> Fn[T_o, P, T_ret]: ...
@overload
def my_deco(
*, arg1: bool = False, arg2: bool = False
) -> Callable[[Fn[T_o, P, T_ret]], Fn[T_o, P, T_ret]]: ...
def my_deco(
fn: Fn[T_o, P, T_ret] | None = None,
/,
*,
arg1: bool = False,
arg2: bool = False
) -> Fn[T_o, P, T_ret] | Callable[[Fn[T_o, P, T_ret]], Fn[T_o, P, T_ret]]:
if fn is not None:
def wrapper(instance: T_o, /, *args: P.args, **kwargs: P.kwargs) -> T_ret:
ret = fn(instance, *args, **kwargs) # Use instance here
print(f"In wrapper({args}, {kwargs}) -> {ret} (__get__)")
print(f" self.funct: {fn}; instance: {instance}")
return ret
reveal_type(wrapper)
return wrapper
else:
def outer(funct: Fn[T_o, P, T_ret]) -> Fn[T_o, P, T_ret]:
def wrapper(self: T_o, /, *args: P.args, **kwargs: P.kwargs) -> T_ret:
ret = funct(self, *args, **kwargs)
print(f"In wrapper({self}, {args}, {kwargs}) -> {ret} (__call__)")
return ret
reveal_type(wrapper)
return wrapper
return outer
[1] 嗯,并非总是如此。您可以直接参数化装饰器,这可能会创建类型不安全的场景。
class A:
@MyDecoCls[A, None, []](arg1=True)
def fn(self, x: int) -> None: ...
fn
仍然有正确的类型(self, x: int) -> None
),但事实上你违反了合同(MyDecoCls
被创建来只处理def _(self) -> None
函数,但你提供了另一个东西它)。不过,这在实际代码中应该不是问题。