
本文介绍在 Python 类型系统中,如何静态约束泛型可调用对象(Callable)的所有参数必须属于 JSON 类型(即 str | int | float | bool | None | Mapping | Sequence),并提供基于 Protocol 的实用解决方案。
本文介绍在 python 类型系统中,如何静态约束泛型可调用对象(`callable`)的所有参数必须属于 `json` 类型(即 `str | int | float | bool | none | mapping | sequence`),并提供基于 `protocol` 的实用解决方案。
在 Python 类型提示中,为泛型可调用对象(如 Callable[P, T])施加「所有参数必须满足某类型约束」的限制,是一个长期存在的挑战。标准 ParamSpec(**P)目前不支持为 *args 或 **kwargs 设置类型边界*(例如 `args: JSON),也无法将参数类型统一绑定到某个协议或联合类型。因此,直接通过def memoize[P, T: JSON](fn: Callable[P, T]) -> ...` 实现参数级约束是不可行的——该语法在当前 mypy / pyright 中不被支持,且 PEP 612 并未定义参数类型的上界机制。
不过,我们可以通过 Protocol 构建一个“仅接受 JSON 参数”的可调用契约,并利用类型检查器的协变/逆变规则,在装饰器返回类型中强制校验。这是一种静态、零运行时开销、符合 PEP 544 的优雅替代方案。
✅ 推荐方案:使用 Protocol 声明 JSON-only 可调用接口
import collections.abc as cx
import typing as t
# 定义递归 JSON 类型(需启用 --enable-incomplete-feature=recursive-types)
type JSON = cx.Mapping[str, JSON] | cx.Sequence[JSON] | str | int | float | bool | None
# 协议:仅允许 *args 和 **kwargs 均为 JSON 类型
class _JSONOnlyCallable(t.Protocol):
def __call__(self, /, *args: JSON, **kwargs: JSON) -> JSON: ...
# 装饰器签名:输入任意 Callable,返回其自身(但类型检查器会按 _JSONOnlyCallable 校验)
def memoize[F: cx.Callable[..., t.Any]](fn: F, /) -> F | _JSONOnlyCallable:
return fn⚠️ 注意:此方案依赖类型检查器对 Protocol.__call__ 的严格参数匹配。*args: JSON 表示所有位置参数必须可赋值给 JSON;**kwargs: JSON 同理。任何违反(如传入 set[int]、datetime、自定义类等)都会在调用点(而非装饰器定义处)报错,精准定位问题。
? 实际使用与错误捕获示例
@memoize
def api_handler(user_id: int, payload: dict[str, str], meta: None) -> str:
return f"OK: {user_id}"
@memoize
def bad_handler(x: set[int]) -> str: # ❌ 类型检查器立即报错
return "never reached"使用 Pyright(推荐)或 mypy(需启用 --enable-incomplete-feature=recursive-types)检查时,bad_handler 的装饰器应用会触发如下错误:
立即学习“Python免费学习笔记(深入)”;
Argument of type "set[int]" cannot be assigned to parameter "args" of type "JSON" in function "__call__"
这说明类型系统已成功拦截非法参数类型,且错误精确指向调用上下文(非装饰器内部),极大提升开发体验。
? 补充说明与注意事项
- 不适用于运行时动态校验:本方案纯静态,不生成运行时逻辑。若需运行时断言(如 isinstance(arg, (str, int, ...))),需额外实现 wrapped_fn 内部的 for arg in args: validate_json(arg),但会牺牲性能且无法替代静态保障。
- Mapping/Sequence 需具体化:JSON 中的 cx.Mapping 和 cx.Sequence 是抽象基类,实际传参需为 dict、list、tuple 等具体 JSON 序列化类型;dataclasses 或 pydantic.BaseModel 实例默认不满足,需显式转换。
- 递归类型支持:Python 3.12+ 原生支持 type JSON = ... 递归别名;旧版本可用 from typing import TYPE_CHECKING + 延迟字符串注解(如 "JSON")配合 --enable-incomplete-feature=recursive-types。
- 装饰器透明性:返回类型为 F | _JSONOnlyCallable 保证了原函数签名完整性,IDE 自动补全和类型推导不受影响。
综上,虽然无法通过 ParamSpec 边界直接约束参数,但借助 Protocol 的结构化契约能力,我们实现了强静态约束 + 精准错误定位 + 零运行时成本的工业级解决方案。这是当前 Python 类型生态下最务实、最推荐的实践路径。










