You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 01:20:54