Python 3.11中如何为带共享参数的装饰器标注Callable类型?
装饰器共享关键字参数的类型注解方案(Python 3.11 + mypy 0.991)
能否保留被装饰函数原签名实现类型检查?
不能。mypy对函数签名与Protocol的匹配规则非常严格:当装饰器要求被装饰函数必须接收共享参数a、b时,若函数签名中未显式声明这些参数,mypy会直接判定类型不兼容——它无法自动推断函数是否能接受未声明的关键字参数,因此无法在完全保留原签名(不加**kw)的前提下通过检查并验证共享参数。
最佳替代方案
以下是几种兼顾类型检查严谨性与代码简洁性的方案:
1. 用ParamSpec + Concatenate绑定共享参数(推荐)
通过ParamSpec捕获被装饰函数的自定义参数,再用Concatenate将共享参数与自定义参数合并,既保留被装饰函数的自定义参数签名,又能让mypy强制检查共享参数是否传入:
from typing import Callable, ParamSpec, TypeVar, Concatenate P = ParamSpec("P") T = TypeVar("T") def my_decorator(func: Callable[Concatenate[dict[str, int], P], T]) -> Callable[Concatenate[int, str, P], T]: def wrapper(a: int, b: str, **kwargs: P.kwargs) -> T: # 处理共享参数a、b shared = {"a": a, "b": b} # 转发所有参数给被装饰函数 return func(**shared, **kwargs) return wrapper # 被装饰函数仅声明自定义参数 @my_decorator def decorated_func(c: bool, **kwargs) -> str: return f"a={kwargs['a']}, b={kwargs['b']}, c={c}" # mypy会强制检查必须传入a、b、c decorated_func(a=1, b="test", c=True) # 合法 decorated_func(c=True) # mypy报错:缺少a和b参数
如果希望被装饰函数显式使用a、b,也可以直接在函数签名中声明,mypy同样会兼容:
@my_decorator def decorated_func(a: int, b: str, c: bool) -> str: return f"a={a}, b={b}, c={c}"
2. 被装饰函数显式声明共享参数(最直接)
如果允许每个被装饰函数显式写出共享参数a、b,类型检查会最清晰,mypy能完全覆盖所有参数的合法性验证:
from typing import Callable, TypeVar T = TypeVar("T") def my_decorator(func: Callable[[int, str, ...], T]) -> Callable[[int, str, ...], T]: def wrapper(a: int, b: str, **kwargs) -> T: return func(a, b, **kwargs) return wrapper @my_decorator def decorated_func(a: int, b: str, c: bool) -> str: return f"a={a}, b={b}, c={c}" decorated_func(a=1, b="test", c=True) # 合法 decorated_func(c=True) # mypy报错
3. Protocol配合**kwargs妥协方案
定义Protocol要求函数接受a、b和任意额外关键字参数,被装饰函数只需添加**kwargs(无需显式声明a、b),即可通过mypy的类型检查:
from typing import Protocol, TypeVar, Callable T = TypeVar("T") class MyCallable(Protocol[T]): def __call__(self, *, a: int, b: str, **kwargs) -> T: ... def my_decorator(func: MyCallable[T]) -> MyCallable[T]: def wrapper(*, a: int, b: str, **kwargs) -> T: return func(a=a, b=b, **kwargs) return wrapper @my_decorator def decorated_func(*, c: bool, **kwargs) -> str: return f"a={kwargs['a']}, b={kwargs['b']}, c={c}" decorated_func(a=1, b="test", c=True) # 合法 decorated_func(c=True) # mypy报错
总结
- 若要完全避免被装饰函数显式声明共享参数,优先选择
ParamSpec+Concatenate的组合,既能保留自定义参数签名,又能实现严格的类型检查; - 若允许显式声明共享参数,直接写在函数签名里是最清晰、最易维护的方案。
内容的提问来源于stack exchange,提问作者chris
相关产品推荐
相关产品推荐

