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

为何可变成员的covariant subtyping在常规Python类中被允许?

可变集合的Invariance

Python中内置可变集合类型为何是invariant,相关PEP文档和Mypy文档已有充分解释,Mypy文档中还给出了专门说明list为何是invariant的示例:

class Shape: ...

class Circle(Shape):
    # rotate方法仅在Circle中定义,Shape没有该方法
    def rotate(self): ...

def add_one(things: list[Shape]) -> None:
    things.append(Shape())

my_circles: list[Circle] = []
add_one(my_circles)      # 看似安全,但
my_circles[-1].rotate()  # 此处会运行失败,因为最后一个元素是Shape而非Circle

我们可以对列表执行append操作;如果list[Circle]被视为list[Shape]的子类型,传入list[Circle]类型的参数调用add_one看似可行。但该函数会向列表中添加一个Shape实例,导致列表不再是list[Circle]类型,后续调用最后一个元素的rotate方法会失败。

简而言之,如果一个复合¹类型是可变的,使其相对于所含类型成为covariant将不符合类型安全。这部分逻辑很清晰。


可变Protocol成员的Invariance

Mypy文档还通过另一个示例解释了为何可变protocol成员的covariant subtyping被视为不安全:

(为方便演示,稍作修改²,使用dataclasses)

from dataclasses import dataclass
from typing import Protocol

class P(Protocol):
    x: float

def fun(arg: P) -> None:
    arg.x = 3.14

@dataclass
class C:
    x: int

c = C(42)
fun(c)    # 这并不安全
c.x << 5  # 因为此处会运行失败!

C看似是P的structural subtype,因为它的x属性是int类型,而int是float的子类型。但Mypy会判定传入C实例调用fun是错误的,因为fun可能(本示例中确实)会修改参数,为x属性赋值float类型的值,后续对x执行位运算会失败。

此处的核心问题依然是类常规成员固有的可变性³,这也合乎逻辑。


常规类中的Covariant Subtyping?

这正是困惑所在:为何同样的逻辑不适用于常规类?来看一个简单示例(使用dataclasses简化代码):

from dataclasses import dataclass

@dataclass
class Foo:
    x: float

@dataclass
class Bar(Foo):
    x: int

def f(obj: Foo) -> None:
    obj.x = 3.14

bar = Bar(1)
f(bar)
bar.x << 5  # 运行时会失败

这段代码可以通过mypy --strict检查且无错误,说明此处对x属性的类型应用了covariant子类型化。

理解将f(bar)判定为错误并不合理,因为Bar显然是Foo的nominal subtype⁴。但为何Bar的定义本身可以被允许?

当然,int是float的子类型,这一点毋庸置疑。但我们此处仍在处理复合类型,难道不应该像之前的案例一样考虑variance吗?

类成员默认是可变的。那么常规类难道不应该像protocol一样,相对于其成员被视为invariant吗?

因此原本预期Bar定义中的x: int会触发错误,逻辑与之前一致,但实际并未触发。这是为什么?


实用性与一致性

这类决策(判定类型是covariant/contravariant/invariant)需要考虑实用性。甚至有观点主张在方法参数类型的语境下违背里氏替换原则(将其视为covariant而非contravariant)。部分论点围绕"世界是协变的"展开,例如2003年的相关论文。无论这些观点是否合理,至少应该保持一致性。

但无法理解上述案例中的一致性何在。


脚注:
¹ 此处的"复合"仅指该类型包含其他具有独立类型的组件。
² Mypy文档中的原始代码片段仅在C的定义中将x = 42设为类属性,足以说明问题,但为了更精准,修改为让C.x成为纯实例属性。
³ Mypy文档的该部分还通过建议将x定义为不可设置的property来解决错误,进一步强调了成员x的可变性是问题根源。
⁴ 相比之下,protocol的structural subtype关系仅在调用/赋值时检查,只有当对象被判定为不是变量类型的子类型时才会触发错误。

内容的提问来源于stack exchange,提问作者Daniel Fainberg

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 09:59:55