如何在Python TypedDict中指定任意额外字段?
Python TypedDict 实现必填固定字段+额外任意字段
需求场景
需要定义TypedDict,同时指定必填的固定字段和允许任意额外字段,用于Flask装饰器的类型标注:Flask路由装饰器会将路径参数作为关键字参数传递,装饰器需要提取并处理其中一个参数(如thing_id),再将转换后的参数(如thing)和剩余参数传递给被装饰函数。
当前代码示例(示意)
from typing import Any, Callable, ParamSpec, Tuple, TypedDict, TypeVar from myproject.models import Thing P0 = ParamSpec("P0") P1 = ParamSpec("P1") R = TypeVar("R") # 无效写法,仅作示意 class IncomingKwargs(TypedDict): thing_id: str **rest class ForwardedKwargs(TypedDict): thing: Thing **rest Decoratee = Callable[P0, R] Decorated = Callable[P1, R] def with_thing() -> Callable[[Decoratee], Decorated]: def decorator(f: Decoratee) -> Decorated: def wrapper(*args: Any, **kwargs: IncomingKwargs) -> R: thing = Thing.from_id(kwargs["thing_id"]) return f(*args, thing=thing, **kwargs["rest"]) return wrapper return decorator # Flask使用示例 @app.route("/things/:thing_id/frobnicate", method=["POST"]) @with_thing() def frobnicate_thing(thing: Thing) -> Tuple[str, int]: return "Thing was frobnicated.", 200
遇到的问题
- 查阅Python官方及mypy文档,未找到直接实现该需求的示例
- 让TypedDict继承
dict无法通过类型检查,也无法正确传递额外字段 TypedDict的total=False选项不符合需求,因为需要thing_id为必填字段
TypeScript 参考实现
在TypeScript中可以通过索引签名轻松实现:
type IncomingKwargs = { thing_id: string, [key: string]: any, } const someKwargs: IncomingKwargs = {thing_id: "12345", description: "A beautiful thing"} // 解构提取必填字段和剩余字段 const { thing_id, ...rest } = someKwargs
解决方案
方案1:使用mypy扩展特性 extra="allow"(推荐)
mypy从0.910版本开始支持TypedDict的extra="allow"参数,允许在保留必填固定字段的同时,接受任意额外字段:
from typing import TypedDict, ParamSpec, TypeVar, Callable, Tuple from typing_extensions import Concatenate, Unpack from myproject.models import Thing P = ParamSpec("P") R = TypeVar("R") # 定义允许额外字段的TypedDict,thing_id为必填 class IncomingKwargs(TypedDict, extra="allow"): thing_id: str class ForwardedKwargs(TypedDict, extra="allow"): thing: Thing # 类型别名:被装饰函数需要接受Thing和其他参数 Decoratee = Callable[Concatenate[Thing, P], R] # 类型别名:装饰后函数接受thing_id和其他参数 Decorated = Callable[Concatenate[str, P], R] def with_thing() -> Callable[[Decoratee], Decorated]: def decorator(f: Decoratee) -> Decorated: def wrapper(*args: Unpack[P], thing_id: str, **kwargs: Any) -> R: thing = Thing.from_id(thing_id) # 传递转换后的参数和所有剩余参数 return f(thing, *args, **kwargs) return wrapper return decorator # Flask使用修正(注意路由参数写法和methods复数) @app.route("/things/<thing_id>/frobnicate", methods=["POST"]) @with_thing() def frobnicate_thing(thing: Thing) -> Tuple[str, int]: return "Thing was frobnicated.", 200
方案2:标准库兼容写法(无需mypy扩展)
如果不想依赖mypy扩展,可以在函数参数中分开标注必填字段和额外关键字参数,类型检查器同样能识别必填项:
from typing import ParamSpec, TypeVar, Callable, Tuple from typing_extensions import Concatenate, Unpack from myproject.models import Thing P = ParamSpec("P") R = TypeVar("R") Decoratee = Callable[Concatenate[Thing, P], R] Decorated = Callable[Concatenate[str, P], R] def with_thing() -> Callable[[Decoratee], Decorated]: def decorator(f: Decoratee) -> Decorated: def wrapper(*args: Unpack[P], thing_id: str, **kwargs: Any) -> R: thing = Thing.from_id(thing_id) return f(thing, *args, **kwargs) return wrapper return decorator
注意事项
- 方案1的
extra="allow"是mypy专属特性,需确保mypy版本≥0.910 - 使用
Concatenate和Unpack需要Python 3.11+,或安装typing_extensions库兼容旧版本 - Flask路由参数的正确写法是
<thing_id>而非:thing_id,methods参数需用复数形式
内容的提问来源于stack exchange,提问作者t-r0d
相关产品推荐
相关产品推荐

