如何为带有可选参数的装饰器添加类型注解?
嘿,你的这个节流装饰器的类型注解写得相当专业!咱们来拆解确认一下,再说说怎么验证它的正确性:
1. 类型变量(TypeVar)的使用恰到好处
你用TypeVar('F', bound=Callable[..., Any])来约束装饰器处理的函数类型,确保装饰后的函数和原函数的签名、返回类型完全一致——这是写类型安全装饰器的标准操作,能让mypy正确追踪原函数的类型信息。
2. 装饰器返回类型适配了两种使用场景
Union[F, Callable[[F], F]]完美覆盖了装饰器的两种调用方式:
- 直接使用
@throttle时,装饰器接收原函数_func并返回装饰后的函数(类型为F) - 带参数使用
@throttle(rate=2.5)时,装饰器先返回一个接收函数的装饰器(类型为Callable[[F], F]),再处理原函数
这完全符合Python装饰器的调用逻辑,mypy能正确识别两种场景的类型。
3. 类型转换(cast)的合理性
你用cast(F, wrapper)告诉mypy:虽然wrapper的签名是(*args: Any, **kwargs: Any) -> Any,但它的实际行为和原函数func完全一致,类型可以安全转换为F。这在装饰器中非常常见,因为我们没法让mypy自动推断wrapper和原函数的签名匹配,手动cast是安全且必要的。
1. 开启mypy严格模式检查
运行mypy --strict your_script.py,严格模式会启用所有严格的类型检查规则(比如disallow_untyped_defs、warn_return_any等)。如果你的注解有隐藏问题,严格模式下大概率会暴露出来。
2. 测试不同的函数签名
给装饰器套上各种类型的函数,验证mypy能否正确推断类型:
import functools import time from typing import TypeVar, Callable, Optional, Union, Any, cast F = TypeVar('F', bound=Callable[..., Any]) def throttle(_func: Optional[F] = None, *, rate: float = 1) -> Union[F, Callable[[F], F]]: """Throttles a function call, so that at minimum it can be called every `rate` seconds.""" def decorator(func: F) -> F: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: time.sleep(rate) return func(*args, **kwargs) return cast(F, wrapper) if _func is None: return decorator else: return decorator(_func) # 测试带参数和返回值的函数 @throttle(rate=2) def add(a: int, b: int) -> int: return a + b result = add(1, 2) # 让mypy检查result的类型是否为int,如果正确,说明注解有效
3. 模拟错误场景验证类型检查
故意传入不符合类型的参数,看mypy是否会报错:
# 这里传入字符串,mypy应该提示参数类型不匹配 result = add("1", "2")
如果mypy能正确报错,说明装饰器保留了原函数的类型检查能力,注解是有效的。
4. 使用mypy的reveal_type查看推断类型
开启--reveal-types参数运行mypy,用reveal_type()查看变量的推断类型:
reveal_type(add) # 应该显示为 Callable[[int, int], int] reveal_type(result) # 应该显示为 int
如果输出符合预期,说明类型注解完全正确。
内容的提问来源于stack exchange,提问作者Laurențiu Andronache

