Python 装饰器类,带或不带参数,装饰类方法,并通过类型检查

问题描述 投票:0回答:1

我想要一个Python装饰器:

  1. 是一个
  2. 可以使用或不使用参数
  3. 通过类型检查
    mypy
  4. 可以装饰类函数

我在这里找到了回答大多数组合的答案,但不是全部四个。

这是我最接近的一次。 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 decorator python-typing mypy
1个回答
0
投票

这是一个令人惊讶的好问题,并不是关于 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
    函数,但你提供了另一个东西它)。不过,这在实际代码中应该不是问题。
    
    
  • ...这个问题非常美观,我的编辑非常漂亮地强调了这一点(公平地说,这通常表明高级元编程或过度工程 - 或两者兼而有之!):

Pretty colors in editor

© www.soinside.com 2019 - 2024. All rights reserved.