如何在Python的TypedDict中标记一组键为互斥?
如何简洁实现TypedDict中键的互斥约束?
我需要定义一个Foo类型的TypedDict,要求除公共键外,只能包含bar和baz中的一个。目前通过拆分多个TypedDict再取联合类型的方式实现:
from typing import TypedDict class Foo1(TypedDict): bar: str class Foo2(TypedDict): baz: int Foo = Foo1 | Foo2 foo_instance_1: Foo = { # 正常通过类型检查 'bar': 'foobar' } foo_instance_2: Foo = { # 正常通过类型检查 'baz': 42 } foo_instance_3: Foo = { # 类型警告:期望类型 'Foo1 | Foo2',实际得到 'dict[str, str | int]' 'bar': 'foobar', 'baz': 42 }
这种方法能满足需求,但实际场景中有多组互斥键时会非常繁琐——比如3组互斥键分别包含2、3、4个键,需要生成234=24个TypedDict类,导致代码文件过长。
我希望能通过更简洁的方式实现,比如在字段定义时标记互斥关系:
class Foo(TypedDict): bar: IncompatibleWith('baz', 'qux')[str] baz: IncompatibleWith('bar', 'qux')[int] qux: IncompatibleWith('bar', 'baz')[bool]
或者用装饰器形式:
@incompatible('bar', 'baz', 'qux') class Foo(TypedDict): bar: str baz: int qux: bool
背景:我正在编写Python代码生成器,用于根据网站API文档生成类型提示。生成所有组合类虽然可行,但会导致文件过于冗长,想知道有没有更简洁的方式标记TypedDict中的一组键为互斥。
解决方案
1. 用Annotated+自定义类型守卫实现无依赖约束
不需要第三方库,通过typing.Annotated标记互斥字段,再配合类型守卫函数完成静态/运行时检查:
from typing import TypedDict, Annotated, TypeGuard, Any, Optional # 自定义注解类,标记互斥键 class IncompatibleWith: def __init__(self, *keys: str): self.excluded = keys # 定义基础TypedDict(包含所有可能字段,total=False表示字段可选) class Foo(TypedDict, total=False): bar: Annotated[str, IncompatibleWith('baz', 'qux')] baz: Annotated[int, IncompatibleWith('bar', 'qux')] qux: Annotated[bool, IncompatibleWith('bar', 'baz')] # 可以添加公共必填键,比如: common_key: str # 自定义类型守卫,验证互斥规则 def is_valid_foo(data: dict[str, Any]) -> TypeGuard[Foo]: mutex_group = {'bar', 'baz', 'qux'} present = [k for k in mutex_group if k in data] # 确保互斥组内仅存在一个键,且公共键存在 return len(present) == 1 and 'common_key' in data # 使用示例 valid_foo = {'common_key': 'test', 'bar': 'foobar'} if is_valid_foo(valid_foo): # 此处valid_foo会被类型检查器识别为符合约束的Foo类型 pass invalid_foo = {'common_key': 'test', 'bar': 'foobar', 'baz': 42} if not is_valid_foo(invalid_foo): # 触发检查失败,符合预期 pass
2. 用pydantic实现更完善的约束(允许引入依赖)
如果项目可以引入pydantic,它能同时提供静态类型提示和运行时验证,写法更简洁:
from pydantic import BaseModel, Field, model_validator from typing import Optional class Foo(BaseModel): # 公共必填键 common_key: str # 互斥字段设为可选,exclude_if_none=True避免序列化时出现None值 bar: Optional[str] = Field(None, exclude_if_none=True) baz: Optional[int] = Field(None, exclude_if_none=True) qux: Optional[bool] = Field(None, exclude_if_none=True) @model_validator(mode='after') def check_mutex_fields(self): present_fields = [f for f in ['bar', 'baz', 'qux'] if getattr(self, f) is not None] if len(present_fields) != 1: raise ValueError("必须且只能提供bar、baz、qux中的一个字段") return self # 使用示例 Foo(common_key='test', bar='foobar') # 正常实例化 Foo(common_key='test', baz=42) # 正常实例化 Foo(common_key='test', bar='foobar', baz=42) # 抛出ValidationError
3. 动态生成联合类型(适配代码生成场景)
针对你编写代码生成器的场景,可以通过动态生成TypedDict联合类型的方式,避免手动编写大量子类:
from typing import TypedDict, Union, TypeVar T = TypeVar('T') def generate_mutex_union(base_name: str, mutex_fields: dict[str, type], common_fields: dict[str, type] = {}) -> Union: typed_dicts = [] # 为每个互斥字段生成带公共键的TypedDict for key, field_type in mutex_fields.items(): # 合并公共键和当前互斥键 fields = {**common_fields, key: field_type} # 动态创建TypedDict类 td_class = TypedDict(f"{base_name}_{key.capitalize()}", fields) typed_dicts.append(td_class) # 返回所有TypedDict的联合类型 return Union[tuple(typed_dicts)] # 生成Foo类型:包含公共键common_key,且bar/baz/qux互斥 Foo = generate_mutex_union( base_name='Foo', mutex_fields={'bar': str, 'baz': int, 'qux': bool}, common_fields={'common_key': str} ) # 使用示例 valid_foo_1: Foo = {'common_key': 'test', 'bar': 'foobar'} # 正常通过类型检查 valid_foo_2: Foo = {'common_key': 'test', 'baz': 42} # 正常通过类型检查 invalid_foo: Foo = {'common_key': 'test', 'bar': 'foobar', 'baz': 42} # 触发类型警告
这种方式在代码生成时只需要调用函数,传入互斥字段和公共字段的配置,就能自动生成对应的联合类型,大幅减少代码量。
内容的提问来源于stack exchange,提问作者InSync
相关产品推荐
相关产品推荐

