Python协议泛型的协变与逆变:为何需将不变量设为逆变?
泛型协议的类型变量方差问题详解
1. 核心规则:函数参数是逆变,返回值是协变
先明确最基础的方差逻辑:
- 协变:如果
A是B的子类,那么X[A]可以替代X[B],这对应返回值场景——比如返回Dog的函数可以替换返回Animal的函数(因为Dog是Animal的子类,返回结果能兼容父类型)。 - 逆变:如果
A是B的子类,那么X[B]可以替代X[A],这对应参数场景——比如接受Animal的函数可以替换接受Dog的函数(因为传给它Dog类型的值,Animal类型的参数完全能接收)。
2. 为什么只有参数的协议要求逆变TypeVar?
看你第一个例子:协议MyProtocol[G]的方法只有参数是G。此时协议作为抽象接口,必须满足子类型传递性——也就是如果一个类实现了MyProtocol[MyType],它应该能被当成MyProtocol[MyType的父类型]使用(比如MyType是Literal["literal_1"],父类型是str)。
但你定义的G是默认的不变(invariant) TypeVar(没指定covariant或contravariant),这就违反了规则:
- 假设
ParentType是MyType的父类型,按照逆变要求,MyProtocol[ParentType]的实现应该能接受任意ParentType类型的参数,但你的CustomClass.do_work只接受MyType,无法兼容更宽泛的ParentType,不符合协议的抽象预期。
所以类型检查器要求你把G声明为逆变:
G = TypeVar("G", contravariant=True)
这样就明确告知检查器:这个TypeVar用于参数位置,遵循逆变规则,实现类的参数类型可以是协议参数类型的子类(因为逆变允许X[父类型]兼容X[子类型]的实现)。
3. 为什么加返回值后警告消失?
当你给方法加上-> G的返回值后,G同时出现在逆变的参数位置和协变的返回值位置。此时逆变和协变的要求相互抵消,TypeVar只能是不变的:
- 如果设为协变,参数位置的逆变要求不满足;
- 如果设为逆变,返回值位置的协变要求不满足;
所以类型检查器认为默认的不变G是合法的——此时协议的子类型规则变成:实现类的参数和返回值必须严格匹配G类型,不能做子类型/父类型的替换,完全符合不变TypeVar的逻辑,因此警告消失。
4. 实际场景示例强化理解
只有参数的逆变场景
from typing import TypeVar, Protocol, Literal T = TypeVar("T", contravariant=True) Animal = str Dog = Literal["dog"] # Dog是Animal的子类 class Feeder(Protocol[T]): def feed(self, animal: T) -> None: ... class DogFeeder: def feed(self, animal: Dog) -> None: print(f"Feeding {animal}") # 逆变规则允许DogFeeder作为Feeder[Animal]使用 feeder: Feeder[Animal] = DogFeeder() # 合法:DogFeeder接受Dog(子类),能兼容Feeder[Animal]接受Animal的要求
同时有参数和返回值的不变场景
class AnimalHandler(Protocol[T]): def handle(self, animal: T) -> T: ... class DogHandler: def handle(self, animal: Dog) -> Dog: ... # 不变规则要求严格匹配 handler: AnimalHandler[Dog] = DogHandler() # 合法 handler: AnimalHandler[Animal] = DogHandler() # 不合法:返回值Dog不能直接当成Animal用,参数Dog也无法接受任意Animal
内容的提问来源于stack exchange,提问作者Tiago Lima
相关产品推荐
相关产品推荐

