Python类型系统运行时无视协变/逆变,仅mypy等静态检查器在类型提示中依TypeVar的covariant/contravariant声明及使用位置(如只读返回值协变、参数逆变)进行校验;自定义泛型默认不变,Sequence等内置类型预设协变。

Python类型系统不支持运行时协变/逆变,只在mypy等检查器中生效
Python本身是动态类型语言,isinstance、issubclass 和实际运行时行为完全遵循类继承树,不区分协变或逆变。所谓“协变/逆变”仅存在于类型提示(typing)被静态检查工具(如 mypy)解析时的规则里——它不影响程序执行,只影响类型检查是否通过。
常见误解是以为加了 Generic[T] 就自动获得协变行为,其实必须显式声明 Contravariant 或 Covariant,且仅对 Protocol 和带 Generic 的类中某些位置有效。
-
Sequence[T]是协变的(Sequence[str]是Sequence[object]的子类型),但这是内置协议的预设规则,不是你定义的类默认拥有的 - 自定义泛型类默认是**不变(invariant)**:哪怕
B继承A,Container[B]和Container[A]也互不兼容 - 只有用
typing.TypeVar(..., covariant=True)声明的类型变量,在支持协变的位置(如只读属性返回值、函数返回类型)才触发协变检查
如何声明协变 TypeVar 并用于 Protocol
真正能体现协变语义的典型场景是只读容器或生产者接口。例如想表达“能产出 Animal 的东西,也能产出它的子类 Dog”,就得靠 Protocol + 协变 TypeVar:
from typing import Protocol, TypeVar, Iterator
<p>class Animal: ...
class Dog(Animal): ...</p><p>T = TypeVar('T', covariant=True)</p><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/00968c3c2c15" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">Python免费学习笔记(深入)</a>”;</p><p>class Producer(Protocol[T]):
def produce(self) -> T: ...</p><p>def feed_animal(p: Producer[Animal]) -> None:
a: Animal = p.produce() # OK</p><h1>mypy 允许传入 Producer[Dog],因为 covariant</h1><p>feed_animal(Producer[Dog]()) # ✅ 类型检查通过
注意:Producer[Dog] 能赋给 Producer[Animal],是因为 produce() 只返回 T,属于“输出位置”,协变才安全。如果协议里还有 consume(x: T) 这种输入位置,协变就会报错。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
为什么不能对 List[T] 做协变赋值?
list 是不变的,哪怕元素类型满足继承关系,List[Dog] 也不能当 List[Animal] 用——因为你可以往 List[Animal] 里 append 任意 Animal 子类实例,但 List[Dog] 容不下 Cat。
-
def take_animals(xs: list[Animal]) -> None: xs.append(Cat())—— 合法操作 - 若允许
take_animals(dogs: list[Dog]),则dogs会被意外插入Cat,破坏类型安全 - 正确替代是用协变只读类型:
Sequence[Dog]≤Sequence[Animal],因为Sequence没有append方法 - 或用
tuple[Dog, ...](协变)代替list[Dog](不变)
逆变只适用于消费型接口,比如函数参数
逆变的典型场景是“接受某种类型的函数”。例如 Callable[[Animal], None] 可以安全地被 Callable[[Dog], None] 替代,因为后者能处理更窄的输入(Dog 是 Animal 的子类,但函数只要求输入是 Dog,传 Animal 反而可能越界)。
要启用逆变,得这样声明:
from typing import Callable, TypeVar
<p>T = TypeVar('T', contravariant=True)</p><p>class Consumer(Protocol[T]):
def consume(self, x: T) -> None:</p><p>def handle_dog(c: Consumer[Dog]) -> None:
c.consume(Dog())</p><h1>mypy 允许把 Consumer[Animal] 传进来,因为逆变</h1><p>handle_dog(Consumer[Animal]()) # ✅
关键点:逆变只在参数位置有意义;一旦你在同一个类型变量上既用作输入又用作输出(比如一个方法既有 def get() -> T 又有 def put(x: T)),那它只能是不变的——mypy 会直接拒绝 covariant=True 和 contravariant=True 同时出现。
实际项目中最容易忽略的是:协变/逆变不是类的属性,而是**类型变量在某个具体位置上的使用方式**。同一个 T 在不同协议或泛型中可以有不同方差,但必须与使用位置语义一致。否则 mypy 报错不是配置问题,而是模型本身矛盾。

















