如何为Python中动态注册的类提供类型安全的工厂函数?
TL;DR
假设一个Python库定义了供第三方代码实现的接口,该如何让这个库提供具备完善静态类型安全性的工厂函数,用于创建这些实现类的实例?
示例
考虑一个定义了Shape协议(Protocol)的库,它对外暴露一个工厂函数,可基于字符串键创建内置或第三方实现的形状实例,并转发构造函数参数。不同的实现类可能有不同的构造参数。
# core.py from typing import Protocol, Any class Shape(Protocol): def area(self) -> float: ... def perimeter(self) -> float: ... SHAPE_REGISTRY: dict[str, type[Shape]] = {} def register_shape(name: str, cls: type[Shape]) -> None: SHAPE_REGISTRY[name] = cls def create_shape(kind: str, **kwargs: Any) -> Shape: cls = SHAPE_REGISTRY[kind] return cls(**kwargs)
以下是一些内置形状:
# shapes.py from core import Shape, register_shape class Circle: def __init__(self, radius: float): self.radius = radius def area(self) -> float: return 3.14 * self.radius ** 2 def perimeter(self) -> float: return 2 * 3.14 * self.radius class Square: def __init__(self, side: float): self.side = side def area(self) -> float: return self.side ** 2 def perimeter(self) -> float: return 4 * self.side register_shape("circle", Circle) register_shape("square", Square)
使用示例:
shape1 = create_shape("circle", radius=1) shape2 = create_shape("square", side=2)
这段代码在运行时完全可行,但希望实现以下静态类型提示能力:
create_shape("circle", radius=1)的返回类型能被推断为Circle- 类型检查器能验证
radius和side这类关键字参数 - 第三方包无需修改核心库即可注册新形状(如
Rectangle、Triangle)
目前找到的唯一类型安全方案是为已知形状添加@overload定义,但这显然无法适配第三方扩展。
问题
是否有办法让这种可动态扩展的工厂函数具备类型安全性,使得静态类型检查器(如mypy或pyright)能够正确推断构造函数参数和返回类型——包括第三方注册的类?
是否需要:
- 重新设计注册表或工厂模式?
- 使用基于
TypedDict的参数系统? - 还是只能通过自定义mypy插件实现?
接受设计层面和类型系统层面的解决方案。
解决方案
1. 泛型+Literal类型映射(适配已知与第三方扩展)
通过类型级注册表配合运行时注册表,让类型检查器关联字符串键与对应类。核心是用泛型绑定具体实现类,同时给第三方留足扩展类型的空间。
修改core.py:
# core.py from typing import Protocol, TypeVar, Literal, overload, Type class Shape(Protocol): def area(self) -> float: ... def perimeter(self) -> float: ... # 绑定具体Shape实现类的类型变量 ShapeT = TypeVar("ShapeT", bound=Shape) # 运行时注册表 SHAPE_REGISTRY: dict[str, Type[Shape]] = {} # 类型层面的注册表协议,第三方可通过子类扩展 class ShapeRegistryTypes(Protocol): circle: Type["Circle"] square: Type["Square"] # 为内置形状添加重载 @overload def create_shape(kind: Literal["circle"], **kwargs) -> "Circle": ... @overload def create_shape(kind: Literal["square"], **kwargs) -> "Square": ... # 兜底重载,兼容第三方未添加类型提示的情况 @overload def create_shape(kind: str, **kwargs) -> Shape: ... def create_shape(kind: str, **kwargs) -> Shape: return SHAPE_REGISTRY[kind](**kwargs) def register_shape(name: str, cls: Type[ShapeT]) -> Type[ShapeT]: SHAPE_REGISTRY[name] = cls return cls
第三方扩展示例(rectangle.py):
# rectangle.py from core import Shape, register_shape, ShapeRegistryTypes, create_shape, overload, Literal class Rectangle(Shape): def __init__(self, width: float, height: float): self.width = width self.height = height def area(self) -> float: return self.width * self.height def perimeter(self) -> float: return 2 * (self.width + self.height) # 注册运行时类 register_shape("rectangle", Rectangle) # 扩展类型注册表,让检查器识别新键 class ExtendedShapeRegistry(ShapeRegistryTypes): rectangle: Type[Rectangle] # 为新形状添加重载 @overload def create_shape(kind: Literal["rectangle"], *, width: float, height: float) -> Rectangle: ...
优点:无需修改核心库即可扩展类型提示;缺点:第三方需自行维护重载与类型注册表。
2. TypedDict参数校验(严格参数检查)
为每个形状定义对应的TypedDict,让工厂函数根据键匹配参数类型,实现严格的参数校验。
修改core.py:
# core.py from typing import Protocol, TypeVar, Literal, overload, TypedDict, Type class Shape(Protocol): def area(self) -> float: ... def perimeter(self) -> float: ... ShapeT = TypeVar("ShapeT", bound=Shape) # 定义各形状的参数类型 class CircleParams(TypedDict): radius: float class SquareParams(TypedDict): side: float # 运行时注册表,同时存储类与参数类型 SHAPE_REGISTRY: dict[str, tuple[Type[Shape], type[TypedDict]]] = {} # 带参数校验的重载 @overload def create_shape(kind: Literal["circle"], *, radius: float) -> "Circle": ... @overload def create_shape(kind: Literal["square"], *, side: float) -> "Square": ... @overload def create_shape(kind: str, **kwargs) -> Shape: ... def create_shape(kind: str, **kwargs) -> Shape: cls, _ = SHAPE_REGISTRY[kind] return cls(**kwargs) def register_shape(name: str, cls: Type[ShapeT], params_type: type[TypedDict]) -> Type[ShapeT]: SHAPE_REGISTRY[name] = (cls, params_type) return cls
第三方扩展时只需定义自己的TypedDict并注册,再添加对应重载即可。优点是参数校验严格,缺点同样需要第三方维护重载。
3. 自定义类型检查器插件(完全动态扩展)
如果希望完全自动化支持第三方扩展,无需手动添加重载,可以编写mypy或pyright插件。
以mypy插件为例,核心逻辑:
- 扫描所有
register_shape调用,提取键与类的映射关系 - 动态为
create_shape生成对应重载 - 校验调用时的参数是否匹配目标类的构造函数
优点:完全动态,无需用户手动维护类型提示;缺点:需要编写和维护插件,技术门槛较高。
4. PEP 695类型别名(Python 3.12+简化方案)
Python 3.12+支持PEP 695,可简化类型注册表定义:
# core.py(Python 3.12+) from typing import Protocol, Type, overload, Literal from typing import TypeAlias, LiteralString class Shape(Protocol): def area(self) -> float: ... def perimeter(self) -> float: ... # 类型层面的注册表 ShapeRegistry: TypeAlias = { "circle": Type["Circle"], "square": Type["Square"], } SHAPE_REGISTRY: dict[str, Type[Shape]] = {} @overload def create_shape[K: LiteralString](kind: K, **kwargs) -> ShapeRegistry[K]: ... def create_shape(kind: str, **kwargs) -> Shape: return SHAPE_REGISTRY[kind](**kwargs)
第三方扩展可直接扩展ShapeRegistry类型别名,配合LiteralString实现精确类型推断,但依赖Python 3.12+版本。
内容的提问来源于stack exchange,提问作者mgab

