如何约束可变参数可调用对象的参数均为JSON子类型?
解决memoize函数的参数JSON类型约束问题
静态类型检查的现状与替代方案
目前Python标准库的ParamSpec(截至3.12版本)不支持为参数集合设置统一的类型边界,没法直接通过memoize[**P: JSON, T: JSON](...)这种语法约束所有参数为JSON子类型。同时issubclass在静态类型检查阶段无法与类型变量兼容,因为此时类型变量是抽象的,无法做实际类型判断。
替代方案:用Protocol定义合规Callable
可以通过Protocol定义一个仅接受JSON类型参数的调用协议,让memoize的入参函数符合该协议:
from typing import Protocol, Callable, TypeVar, Union, Mapping, Sequence JSON = Union[Mapping[str, "JSON"], Sequence["JSON"], str, int, float, bool, None] T = TypeVar("T", bound=JSON) class JSONCallable(Protocol): def __call__(self, *args: JSON, **kwargs: JSON) -> T: ... def memoize(fn: JSONCallable[T]) -> JSONCallable[T]: # 实现memoize逻辑 cache = {} def wrapper(*args: JSON, **kwargs: JSON) -> T: key = (args, frozenset(kwargs.items())) if key not in cache: cache[key] = fn(*args, **kwargs) return cache[key] return wrapper
局限:该方案会丢失原函数的精确参数签名(比如参数名称、个数),静态检查时无法提示原函数的参数细节。
运行时参数类型检查实现
如果静态约束无法满足需求,可以在memoize内部创建包装器前,通过解析函数签名的类型注解做运行时检查:
import inspect from typing import Union, Mapping, Sequence, Callable, ParamSpec, TypeVar, get_args, get_origin JSON = Union[Mapping[str, "JSON"], Sequence["JSON"], str, int, float, bool, None] P = ParamSpec("P") T = TypeVar("T", bound=JSON) def is_json_type(tp) -> bool: """判断给定类型是否为JSON的子类型""" # 处理Union类型 if get_origin(tp) is Union: return all(is_json_type(arg) for arg in get_args(tp)) # 处理泛型容器类型 origin = get_origin(tp) if origin is not None: if origin is Mapping: key_tp, val_tp = get_args(tp) return key_tp is str and is_json_type(val_tp) if origin is Sequence: elem_tp, = get_args(tp) return is_json_type(elem_tp) # 处理基础类型与自定义子类 return tp in (str, int, float, bool, type(None)) or issubclass(tp, (str, int, float, bool)) def memoize(fn: Callable[P, T]) -> Callable[P, T]: # 运行时验证所有参数类型 sig = inspect.signature(fn) for param in sig.parameters.values(): param_tp = param.annotation if param_tp is inspect.Parameter.empty: raise TypeError(f"参数 {param.name} 未提供类型注解,无法验证是否为JSON子类型") if not is_json_type(param_tp): raise TypeError(f"参数 {param.name} 的类型 {param_tp} 不属于JSON子类型") # 标准memoize包装逻辑 cache = {} def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: key = (args, frozenset(kwargs.items())) if key not in cache: cache[key] = fn(*args, **kwargs) return cache[key] return wrapper
运行时检查注意事项
- 无类型注解的参数会直接抛出错误,若需宽松处理可改为警告,或默认视为合规,但存在类型风险。
- 支持泛型容器类型(如
list[JSON]、dict[str, int])的检查。 - 支持自定义JSON子类型(如继承
str的自定义类)的检查。
内容的提问来源于stack exchange,提问作者AAZ
相关产品推荐
相关产品推荐

