如何让mypy识别lazy_decorator对函数参数类型的扩展,实现LazyOp参数的透明支持?
如何让mypy识别lazy_decorator对函数参数类型的扩展,实现LazyOp参数的透明支持?
解决方案:通过类型别名与参数规范映射,让mypy自动兼容LazyOp参数
要实现你的需求,我们需要修改装饰器的类型注解,让mypy理解:被装饰函数的每个参数既可以接受原始类型,也可以接受对应的LazyOp类型。核心思路是用TypeVar、ParamSpec和类型别名来动态扩展参数类型范围,同时保留原函数的类型注解可读性。
以下是修改后的完整代码,附带详细说明:
from functools import wraps from typing import Any, Callable, Generic, ParamSpec, TypeVar, Union, TypeAlias T = TypeVar("T") P = ParamSpec("P") # 定义辅助类型:表示「原始类型T」或「对应的LazyOp[T]」 LazyOr: TypeAlias = Union[T, "LazyOp[T]"] class LazyOp(Generic[T]): def __init__(self, func: Callable[..., T], args: tuple[Any, ...], kwargs: dict[str, Any]): self.func = func self.args = args self.kwargs = kwargs def __call__(self) -> T: args = [a() if isinstance(a, LazyOp) else a for a in self.args] kwargs = {k: v() if isinstance(v, LazyOp) else v for k, v in self.kwargs.items()} return self.func(*args, **kwargs) # 自定义ParamSpec,将原函数的所有参数类型转换为LazyOr包裹的版本 class LazyParamSpec(ParamSpec): args: tuple[LazyOr[t] for t in P.args] kwargs: dict[str, LazyOr[t] for t in P.kwargs.values()] def lazy_decorator(func: Callable[P, T]) -> Callable[LazyParamSpec[P], LazyOp[T]]: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> LazyOp[T]: # 运行时逻辑完全不变,仅修改类型注解 return LazyOp(func, args, kwargs) return wrapper @lazy_decorator def lazy_sum(a: int, b: int) -> int: return a + b lazy_result = lazy_sum(1, 2) # 现在mypy不会报错:LazyOp[int] 被自动识别为合法参数 lazy_result = lazy_sum(lazy_result, 4) result = lazy_result() print(result) # 输出7
关键设计细节(满足你的所有需求):
保留原始类型注解
被装饰的函数lazy_sum仍然显示接受int类型参数,开发者看到的还是熟悉的原始签名,@wraps也保留了原函数的元数据。自动兼容LazyOp参数
通过LazyParamSpec,mypy会自动将原函数的每个参数类型扩展为「原始类型 + LazyOp类型」的联合,无需手动修改每个函数的签名。无需手动调整
所有类型转换逻辑都封装在装饰器中,被装饰函数完全不需要修改自己的类型注解,直接使用即可。无缝集成
- 不依赖任何外部mypy插件,仅使用Python标准库的类型注解特性
- 运行时代码完全不变,没有任何性能损失
- 无需大幅重构现有代码,仅替换装饰器的类型注解部分即可
兼容旧版mypy的备选方案
如果你使用的mypy版本不支持自定义ParamSpec子类(低于0.1000版本),可以改用TypeVarTuple和Unpack实现相同效果:
from functools import wraps from typing import Any, Callable, Generic, TypeVar, Union, TypeAlias, TypeVarTuple, Unpack T = TypeVar("T") Ts = TypeVarTuple("Ts") LazyOr: TypeAlias = Union[T, "LazyOp[T]"] class LazyOp(Generic[T]): # 保持原实现不变 def __init__(self, func: Callable[..., T], args: tuple[Any, ...], kwargs: dict[str, Any]): self.func = func self.args = args self.kwargs = kwargs def __call__(self) -> T: args = [a() if isinstance(a, LazyOp) else a for a in self.args] kwargs = {k: v() if isinstance(v, LazyOp) else v for k, v in self.kwargs.items()} return self.func(*args, **kwargs) def lazy_decorator(func: Callable[Unpack[Ts], T]) -> Callable[tuple[LazyOr[Unpack[Ts]]], LazyOp[T]]: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> LazyOp[T]: return LazyOp(func, args, kwargs) return wrapper # 用法完全一致 @lazy_decorator def lazy_sum(a: int, b: int) -> int: return a + b
备注:内容来源于stack exchange,提问作者Phinnik
相关产品推荐
相关产品推荐

