如何让Unique等泛型代理类型对静态类型检查器透明?
实现透明的ORM约束泛型类型
要让Unique[T]、Index[T]和Primary[T]这类泛型类型在IDE和静态检查器中被视为T,同时在运行时保留约束标记,最简洁且符合PEP标准的方案是利用typing.Annotated(PEP 593)来封装这些约束。下面是具体实现思路和代码:
方案一:基于Annotated的透明代理实现
核心思路是通过__class_getitem__让约束类返回Annotated[T, Marker]——类型检查器会自动忽略Annotated的元数据部分,将其视为T,而我们可以在运行时提取元数据来识别约束。
完整代码示例
from typing import TypeVar, Generic, Annotated, get_type_hints import inspect T = TypeVar('T') # 定义运行时识别约束的标记类 class UniqueMarker: pass class IndexMarker: pass class PrimaryMarker: pass class Unique(Generic[T]): @classmethod def __class_getitem__(cls, item): # 返回Annotated类型,静态检查器会将其视为item本身 return Annotated[item, UniqueMarker] def __new__(cls, *args, **kwargs): raise TypeError(f'cannot instantiate {cls.__name__}') class Index(Generic[T]): @classmethod def __class_getitem__(cls, item): return Annotated[item, IndexMarker] def __new__(cls, *args, **kwargs): raise TypeError(f'cannot instantiate {cls.__name__}') class Primary(Generic[T]): @classmethod def __class_getitem__(cls, item): return Annotated[item, PrimaryMarker] def __new__(cls, *args, **kwargs): raise TypeError(f'cannot instantiate {cls.__name__}') # 示例ORM模型 class User: id: Primary[int] username: Unique[str] email: Index[str] age: int # 运行时提取字段约束的工具函数 def parse_model_constraints(model_cls): # include_extras=True 保留Annotated的完整信息 type_hints = get_type_hints(model_cls, include_extras=True) field_constraints = {} for field_name, hint in type_hints.items(): base_type = hint has_unique = False has_index = False has_primary = False # 检查是否是Annotated类型 if isinstance(hint, Annotated): base_type = hint.__args__[0] markers = hint.__args__[1:] has_unique = any(isinstance(m, UniqueMarker) for m in markers) has_index = any(isinstance(m, IndexMarker) for m in markers) has_primary = any(isinstance(m, PrimaryMarker) for m in markers) field_constraints[field_name] = { "type": base_type, "primary": has_primary, "unique": has_unique, "index": has_index } return field_constraints # 测试输出 print(parse_model_constraints(User))
方案优势
- 类型检查友好:所有主流静态检查器(mypy、Pyright、Pytype)和IDE(PyCharm、VS Code)都会将
Unique[str]识别为str,代码补全、类型提示完全正常。 - 运行时可识别:通过
get_type_hints并设置include_extras=True,可以轻松提取字段的约束信息,用于生成数据库表结构。 - 写法简洁:用户只需写
Unique[str],无需额外的Annotated语法,符合直觉。
兼容性说明
如果需要支持Python 3.8及以下版本,可以从typing_extensions库导入Annotated(该库是官方维护的,兼容所有Python版本)。
方案二:不依赖Annotated的兼容声明(适合老版本)
如果无法使用Annotated,可以通过Protocol的__subclasshook__让类型检查器认为约束类与T兼容,但这种方案兼容性较差,且实现复杂:
from typing import TypeVar, Generic, Protocol from typing_extensions import Protocol # Python 3.8及以下需要 T = TypeVar('T') class Unique(Generic[T]): def __new__(cls, *args, **kwargs): raise TypeError(f'cannot instantiate {cls.__name__}') # 定义协议,让类型检查器认为任何T的实例都匹配Unique[T] class _UniqueCompatible(Protocol[T]): @classmethod def __subclasshook__(cls, subclass): return issubclass(subclass, T) # 告诉类型检查器Unique[T]与_UniqueCompatible[T]等价(需配合mypy等检查器的配置) Unique = Unique[_UniqueCompatible[T]] # 仅用于类型推断,运行时不影响
这种方案需要依赖类型检查器对Protocol的支持,且不同工具的表现可能不一致,因此优先推荐方案一。
内容的提问来源于stack exchange,提问作者Jasmijn
相关产品推荐
相关产品推荐

