如何实现支持多装饰构建参数类型Union且单装饰校验正常的装饰器?
解决方案:使用TypeVarTuple + Unpack实现兼容单/多装饰的类型安全注册器
核心思路是利用Python 3.10+的TypeVarTuple和Unpack特性,收集多次装饰传入的所有Event类型,将处理器函数的参数约束为这些类型的联合类型,同时保证单装饰场景下的严格校验。
实现代码
from typing import TypeVar, Callable, ParamSpec, TypeVarTuple, Unpack, Union # 基础Event类和类型变量 E = TypeVar('E', bound='Event') class Event: pass class A(Event): pass class B(Event): pass # 定义用于收集多装饰Event类型的TypeVarTuple Events = TypeVarTuple('Events') P = ParamSpec('P') def register(event_type: type[Unpack[Events]]) -> Callable[[Callable[[Union[Unpack[Events]], ...], None]], Callable[[Union[Unpack[Events]], ...], None]]: def decorator(func: Callable[[Union[Unpack[Events]], ...], None]) -> Callable[[Union[Unpack[Events]], ...], None]: # 这里添加实际的注册逻辑(比如将函数绑定到事件类型) return func return decorator
场景测试
1. 单装饰场景(严格类型校验)
# ✅ 合法:参数类型与装饰器指定的A一致 @register(A) def handle_a(event: A) -> None: pass # ❌ 类型错误:参数是B,但装饰器要求A @register(A) def handle_b(event: B) -> None: pass
2. 多装饰场景(支持Union参数)
# ✅ 合法:参数是A|B,匹配两次装饰的类型 @register(A) @register(B) def handle_either(event: A | B) -> None: pass # ✅ 合法:参数是A(属于A|B的子集) @register(A) @register(B) def handle_a_only(event: A) -> None: pass # ❌ 类型错误:参数是未注册的C类型 class C(Event): pass @register(A) @register(B) def handle_c(event: C) -> None: pass
方案优势
- 单装饰时,
Union[Unpack[Events]]等价于单个Event类型,保持严格的参数校验,不会出现register3的错误提示问题。 - 多装饰时,自动将所有注册的Event类型合并为Union,完美适配参数为联合类型的处理器函数,解决register1的误报问题。
- 相比ParamSpec实现的register2,通过明确约束参数为注册类型的联合,能准确触发预期的类型错误。
可选:严格约束参数必须是全量Union
如果需要强制处理器函数的参数必须是所有注册类型的完整联合(不允许子集),可以调整装饰器的类型签名:
def register(event_type: type[Unpack[Events]]) -> Callable[[Callable[[Union[Unpack[Events]]], None]], Callable[[Union[Unpack[Events]]], None]]: def decorator(func: Callable[[Union[Unpack[Events]]], None]) -> Callable[[Union[Unpack[Events]]], None]: return func return decorator
此时handle_a_only会触发类型错误,因为参数是A而非A|B。
内容的提问来源于stack exchange,提问作者MaPePeR
相关产品推荐
相关产品推荐

