如何在Python中为绑定到Protocol的枚举值添加类型提示
解决方案
方案一:放弃Enum,用Final类属性(推荐)
既然你要的是固定键、能通过属性访问、类型安全的结构,没必要硬套Enum。直接用类加Final类型注解,就能在定义阶段触发类型检查,代码还更简洁:
from __future__ import annotations import typing as t from typing import Final class Viewable(t.Protocol): def view(self) -> None: ... class MyNumber: def __init__(self, value: int): self.value = value def view(self) -> None: print(self.value) class MyString: def __init__(self, value: str): self.value = value def view(self) -> None: print(self.value) one = MyNumber(1) two = MyNumber(2) three = MyString("three") four = 4 class MyConfig: ONE: Final[Viewable] = one TWO: Final[Viewable] = two THREE: Final[Viewable] = three FOUR: Final[Viewable] = four # <--- mypy直接在这儿报错:int类型不匹配Viewable if __name__ == "__main__": MyConfig.ONE.view() MyConfig.TWO.view() MyConfig.THREE.view() MyConfig.FOUR.view()
优势:
- 定义阶段直接揪出类型错误,完全符合你的需求
- 不用给每个值挨个加注解,只需要给类属性统一加
Final[Viewable] - 访问方式和Enum一模一样(用
.点属性),结构更轻量
方案二:自定义Enum元类实现类型检查
如果一定要保留Enum的特性(比如成员是Enum实例、支持枚举的各种特殊方法),可以自定义元类,在枚举创建时做类型验证,同时给mypy提供类型提示:
from __future__ import annotations import typing as t from enum import Enum, EnumMeta from typing import TypeVar, Generic, cast class Viewable(t.Protocol): def view(self) -> None: ... T = TypeVar("T", bound=Viewable) class TypedEnumMeta(EnumMeta, Generic[T]): def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, t.Any]) -> TypedEnumMeta[T]: # 运行时验证每个成员的类型 for key, value in namespace.items(): if not key.startswith("_"): # 跳过私有属性 if not isinstance(value, Viewable): raise TypeError(f"枚举成员 {key} 必须实现Viewable协议,实际类型是 {type(value)}") return cast(TypedEnumMeta[T], super().__new__(cls, name, bases, namespace)) # 给mypy做类型提示,让它知道成员的类型是T def __getitem__(self, item: str) -> T: ... def __getattr__(self, item: str) -> T: ... class MyEnum(Enum, metaclass=TypedEnumMeta[T]): ONE = MyNumber(1) TWO = MyNumber(2) THREE = MyString("three") FOUR = 4 # <--- 运行时会抛出TypeError,mypy也会在这儿报错 # 原MyNumber、MyString定义不变 class MyNumber: def __init__(self, value: int): self.value = value def view(self) -> None: print(self.value) class MyString: def __init__(self, value: str): self.value = value def view(self) -> None: print(self.value) if __name__ == "__main__": MyEnum.ONE.view() # 不用写.value,直接调用方法就行 MyEnum.TWO.view() MyEnum.THREE.view() MyEnum.FOUR.view()
说明:
- 自定义元类
TypedEnumMeta在枚举创建时(运行时)就会检查每个成员是否符合Viewable - 通过
__getattr__和__getitem__的类型提示,让mypy在定义阶段就发现类型错误 - 访问时不用再写
.value,直接调用成员的方法就行
为啥原生Enum+Generic不行?
原生Enum的元类会把每个成员包装成Enum实例,mypy只会在你访问.value属性的时候才检查类型,所以错误只会在调用阶段才冒出来。上面的方案要么绕开Enum的包装(方案一),要么通过元类和类型提示提前把错误揪出来(方案二)。
内容的提问来源于stack exchange,提问作者Susensio
相关产品推荐
相关产品推荐

