如何在Python中将装饰器标注为可调用对象

编程语言 2026-07-10

我想定义一个装饰器,也就是一个可调用的对象,它返回另一个可调用的对象。

我想用一种能够在装饰时就验证参数和返回值类型正确的方法,同时在调用时也能验证。

这本可以通过向ProtocolCallable添加一个 __get_ 方法来实现,但我得再次定义参数和返回值;如果参数很多,可能会非常冗长。

这是一个在运行时能正常工作,但在对my_method的调用中mypy会报错的示例(参数1 的类型为 "Callable[[MyClass, int], Coroutine[Any, Any, str]]";期望为 "MethodCallable")

from typing import Any, Awaitable, Callable, Protocol

class MethodCallable(Protocol):
    def __call__(self, some_arg: int) -> Awaitable[str]: ...

def some_decorator() -> Callable[[MethodCallable], MethodCallable]:
    def wrapper(func: MethodCallable) -> MethodCallable:
        print("Decorator is applied.")
        return func
    return wrapper

class MyClass:
    @some_decorator()
    async def my_method(self, some_arg: int) -> str:
        return f"Result: {some_arg}"

# Example usage
if __name__ == "__main__":
    obj = MyClass()
    print(obj.my_method(some_arg=1))  # Output: Result: 1

我希望如果我这样做,mypy能发出警告:

obj.my_method("str") # input should be int

以及

class MyClass:
    @some_decorator()
    async def my_method(self, some_arg: str) -> str: # some_arg should be int

一个解决办法可以是:

class MethodCallable(Protocol):
     def __call__(self, __real_self: Any, __some_arg: int) -> Awaitable[str]: ...

    def __get__(self, instance: Any, owner: Any) -> "Callable[[int], Awaitable[str]]": ...

但这将需要两次定义参数和返回值,且无法确保两者一致,因此会让mypy通过,但在运行时会失败

from typing import Any, Awaitable, Callable, Protocol
class MethodCallable(Protocol): 
    def __call__(self, __real_self: Any, __some_arg: int) -> Awaitable[str]: ...
    def __get__(self, instance: Any, owner: Any) -> "Callable[[int], Awaitable[str]]": ...


def some_decorator() -> Callable[[MethodCallable], MethodCallable]:
def wrapper(func: MethodCallable) -> MethodCallable:
print("Decorator is applied.")
return func
return wrapper

class MyClass:
@some_decorator()
async def my_method(self, some_arg: int) -> str:
return f"Result: {some_arg}"

# Example usage

if __name__ == "__main__":
obj = MyClass()
print(obj.my_method(1, "a"))  # Output: Result: 1

解决方案

对于来看这条回答的各位,这个答案并没有满足原问题者的要求,因为他们需要对被装饰方法的关键字参数进行类型检查,而 Callable 不支持。这里保留原始答案,以示范如何用位置参数解决问题。


这个对你原始示例所做的修改对我来说有效。这样做能实现你的目标吗?

from typing import Any, Awaitable, Callable

def some_decorator() -> Callable[[Callable[[Any,int],Awaitable[str]]],Callable[[Any,int],Awaitable[str]]]:
    def wrapper(func: Callable[[Any,int],Awaitable[str]]) -> Callable[[Any,int],Awaitable[str]]:
        print("Decorator is applied.")
        return func
    return wrapper

class MyClass:
    @some_decorator()
    async def my_method(self, some_arg: int) -> str:
        return f"Result: {some_arg}"
    # the following fails with:
    # Argument 1 has incompatible type "Callable[[MyClass, str], Coroutine[Any, Any, str]]"; expected "Callable[[Any, int], Awaitable[str]]"  [arg-type]
    @some_decorator()
    async def my_method_2(self, wrong_arg: str) -> str:
        return f"Result: {wrong_arg}"
    # the following fails with:
    # Argument 1 has incompatible type "Callable[[MyClass, int], Coroutine[Any, Any, int]]"; expected "Callable[[Any, int], Awaitable[str]]"  [arg-type]
    @some_decorator()
    async def my_method_3(self, some_arg: int) -> int:
        return some_arg + 1
    # the following fails with:
    # Argument 1 has incompatible type "Callable[[MyClass, int], str]"; expected "Callable[[Any, int], Awaitable[str]]"  [arg-type]
    @some_decorator()
    def my_method_4(self, some_arg: int) -> str:
        return f"Result: {some_arg}"

# Example usage
if __name__ == "__main__":
    obj = MyClass()
    print(obj.my_method(1))  # Output: Result: 1
    # the following fails with:
    # Argument 1 to "my_method" of "MyClass" has incompatible type "str"; expected "int"  [arg-type]
    print(obj.my_method("foo"))
    # the following fails with:
    # Unsupported operand types for + ("Awaitable[str]" and "int")  [operator]
    print(obj.my_method(1)+1)

此外——就最佳实践而言,注意最近的Python版本已将 typing.Callable 废弃,改为使用 collections.abc.Callable,同样地 Awaitable 也是如此:

from collections.abc import Awaitable, Callable
from typing import Any
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章