Python泛型中能否在一个TypeVar的bound里引用另一个TypeVar?
问题描述
我有一个泛型Base类,它会实例化Factory类。Factory类由Base的子类提供给Base,随后Base会将该实例传递给某些子类方法。我希望Base中Factory的类型依赖于一个更早定义的TypeVar,请问在Python中是否有办法实现这一点?几年前的Stack Overflow问答指出无法实现,现在情况是否有变化?如果没有,是否有替代方案(比如使用Protocol)?
我尝试了如下Python代码:
from typing import Generic, TypeVar T = TypeVar("T") class Factory(Generic[T]): def __init__(self, session: Session) -> None: self.session = session def get(self, id: int) -> T | None: return None def all(self) -> list[T]: return [] class Session: def query(self, query: object) -> object: pass F = TypeVar("F", bound=Factory[T]) class Base(Generic[T, F]): def __init__(self, Factory: type[F]) -> None: self.Factory = Factory def doSomething(self, factory: F, thing: T) -> None: pass def run(self, session: Session) -> None: factory = self.Factory(session) for thing in factory.all(): self.doSomething(factory, thing) class User: userId: int name: str class UserFactory(Factory[User]): def login(self, user: User) -> None: self.session.query('whatever') class UserThing(Base[User, UserFactory]): def __init__(self): super().__init__(UserFactory) def doSomething(self, factory: UserFactory, user: User) -> None: factory.login(user)
运行类型检查时出现错误:
main.py:19: error: Type variable "__main__.T" is unbound [valid-type] main.py:19: note: (Hint: Use "Generic[T]" or "Protocol[T]" base class to bind "T" inside a class) main.py:19: note: (Hint: Use "T" in function signature to bind "T" inside a function) Found 1 error in 1 file (checked 1 source file)
我更熟悉TypeScript,可以轻松实现该需求,参考代码如下:
class Factory<T> { constructor(protected session: Session) {} get(id: number): T | null { return null } all(): T[] { return [] } } interface Session { query(query: unknown): unknown } abstract class Base<T, F extends Factory<T>> { constructor(public readonly Factory: { new (session:Session): F }) { } abstract doSomething(factory: F, thing: T): void public run(session: Session) { const factory = new this.Factory(session) for (const thing of factory.all()) { this.doSomething(factory, thing) } } } interface User { userId: number name: string } class UserFactory extends Factory<User> { login(user: User) { this.session.query('whatever') } } class UserThing extends Base<User, UserFactory> { constructor() { super(UserFactory) } doSomething(factory: UserFactory, user: User) { factory.login(user) } }
解答
当前Python类型系统的限制
截至Python 3.12+以及最新版mypy,仍然不支持在TypeVar的bound约束中引用另一个未绑定的TypeVar(即F = TypeVar("F", bound=Factory[T])这种写法)。T在这里没有被绑定到任何泛型上下文,类型检查器无法推断它的具体类型,所以会抛出"Type variable is unbound"的错误。几年前Stack Overflow上的结论目前仍然有效。
替代方案:使用Protocol约束Factory类型
我们可以通过Protocol来定义Factory必须满足的接口,同时将其与T类型关联,这样既能保持类型安全,又能绕过TypeVar bound的限制。
修改后的Python代码如下:
from typing import Generic, TypeVar, Protocol, Type T = TypeVar("T") class Session: def query(self, query: object) -> object: pass # 定义Factory的Protocol,明确关联T类型 class FactoryProtocol(Protocol[T]): def __init__(self, session: Session) -> None: ... def get(self, id: int) -> T | None: ... def all(self) -> list[T]: ... # 基础Factory类,实现Protocol class Factory(Generic[T], FactoryProtocol[T]): def __init__(self, session: Session) -> None: self.session = session def get(self, id: int) -> T | None: return None def all(self) -> list[T]: return [] # 重新定义Base的泛型参数,F约束为FactoryProtocol[T] F = TypeVar("F", bound=FactoryProtocol[T]) class Base(Generic[T, F]): def __init__(self, factory_cls: Type[F]) -> None: self.factory_cls = factory_cls def doSomething(self, factory: F, thing: T) -> None: pass def run(self, session: Session) -> None: factory = self.factory_cls(session) for thing in factory.all(): self.doSomething(factory, thing) class User: userId: int name: str class UserFactory(Factory[User]): def login(self, user: User) -> None: self.session.query('whatever') class UserThing(Base[User, UserFactory]): def __init__(self): super().__init__(UserFactory) def doSomething(self, factory: UserFactory, user: User) -> None: factory.login(user)
另一种简化方案:省略F的显式bound
如果不需要严格约束F必须是Factory的子类,也可以直接将Base定义为Generic[T, F],并在方法中利用类型推断来关联F和T的关系。这种写法更简洁,同样能通过类型检查:
from typing import Generic, TypeVar, Type T = TypeVar("T") F = TypeVar("F") class Session: def query(self, query: object) -> object: pass class Factory(Generic[T]): def __init__(self, session: Session) -> None: self.session = session def get(self, id: int) -> T | None: return None def all(self) -> list[T]: return [] class Base(Generic[T, F]): def __init__(self, factory_cls: Type[F]) -> None: self.factory_cls = factory_cls def doSomething(self, factory: F, thing: T) -> None: pass def run(self, session: Session) -> None: factory = self.factory_cls(session) # 类型检查器会推断factory.all()返回的是list[T] for thing in factory.all(): self.doSomething(factory, thing) # 后续User、UserFactory、UserThing的定义保持不变
以上两种方案都能实现你想要的类型关联效果,同时通过mypy等类型检查工具的验证。
内容的提问来源于stack exchange,提问作者David Loehr

