Python 的协变、逆变与不变

标签:Python

前几天在使用 httpx 时,发现它的代理参数声明的类型是 ProxiesTypes
URLTypes = Union["URL", str]
ProxyTypes = Union[URLTypes, "Proxy"]
ProxiesTypes = Union[ProxyTypes, Dict[URLTypes, Union[None, ProxyTypes]]]
可以看出,dict[str, str] 应该是符合它的参数签名的,然而我传入一个 dict[str, str] 参数后,Pylance 却会报错,这让我大为不解。

于是我又尝试简化了一下这个问题:
from typing import Mapping

a: dict[int, int] = {}
b: dict[int, int | str] = a  # error:
# Expression of type "dict[int, int]" is incompatible with declared type "dict[int, int | str]"
#   "dict[int, int]" is incompatible with "dict[int, int | str]"
#     Type parameter "_VT@dict" is invariant, but "int" is not the same as "int | str"
#     Consider switching from "dict" to "Mapping" which is covariant in the value type
c: Mapping[int, int | str] = a
d: Mapping[int | str, int] = a  # error:
# Expression of type "dict[int, int]" is incompatible with declared type "Mapping[int | str, int]"
#   "dict[int, int]" is incompatible with "Mapping[int | str, int]"
#     Type parameter "_KT@Mapping" is invariant, but "int" is not the same as "int | str"
是不是很奇怪,为啥 dict[int, int]dict[int, int | str]Mapping[int | str, int] 都不兼容,而与 Mapping[int, int | str] 兼容?

为此我查了很多资料,终于在这篇文章里找到了答案。

先定义如下两个类:
class Animal:
    def eat(self): ...


class Bird(Animal):
    def fly(self): ...
那么我们在所有需要用 Animal 的实例的地方,应该都能用 Bird 的实例,而反过来则不行:
bird: Bird = Bird()
bird.eat()
bird.fly()

animal: Animal = bird
animal.eat()
animal.fly()  # error: "Animal" has no attribute "fly"
函数参数也是如此:
def let_it_eat(animal: Animal):
    animal.eat()


def let_it_fly(bird: Bird):
    bird.fly()


let_it_eat(bird)
let_it_eat(animal)
let_it_fly(bird)
let_it_fly(animal)  # error: Argument 1 to "fly" has incompatible type "Animal"; expected "Bird"
从数学意义上来看,Bird 其实是 Animal 的子集(subset),这里我们称之为子类型(sub type)。

这也可以适用于泛型类型:
def let_them_eat(animals: tuple[Animal]):
    for animal in animals:
        animal.eat()


def let_them_fly(birds: tuple[Bird]):
    for bird in birds:
        bird.fly()


let_them_eat((bird,))
let_them_eat((animal,))
let_them_fly((bird,))
let_them_fly((animal,))  # error: Argument 1 to "let_them_fly" has incompatible type "tuple[Animal]"; expected "tuple[Bird]"
也就是说,tuple[Bird] 也是 tuple[Animal] 的子集和子类型。
如果 SubTypeSuperType 的子类型,且 GenType[SubType, ...] 也是 GenType[SuperType, ...] 的子类型,那么泛型类型 GenType[T, ...] 对类型 T 是协变(covariant)的。
除了 Tuple 外,还有 UnionFrozenSet 等类型也是协变的。

可是 Callable 的参数类型却有点违反直觉:
AnimalCommand = Callable[[Animal], None]
BirdCommand = Callable[[Bird], None]

bird_command: BirdCommand = let_it_fly
bird_command = let_it_eat
animal_command: AnimalCommand = let_it_eat
animal_command = let_it_fly  # error:
# Expression of type "(bird: Bird) -> None" is incompatible with declared type "AnimalCommand"
#   Type "(bird: Bird) -> None" is incompatible with type "AnimalCommand"
#     Parameter 1: type "Animal" is incompatible with type "Bird"
#       "Animal" is incompatible with "Bird"
AnimalCommand 可以用在所有需要 BirdCommand 的地方,而反过来却不行。

再看如下的例子:
def command_animal(animal: Animal, command: AnimalCommand):
    command(animal)


def command_bird(bird: Bird, command: BirdCommand):
    command(bird)


command_animal(animal, let_it_eat)
command_animal(animal, let_it_fly)  # error:
# Argument of type "(bird: Bird) -> None" cannot be assigned to parameter "command" of type "AnimalCommand" in function "command_animal"
#   Type "(bird: Bird) -> None" is incompatible with type "AnimalCommand"
#     Parameter 1: type "Animal" is incompatible with type "Bird"
#       "Animal" is incompatible with "Bird"
command_bird(bird, let_it_eat)
command_bird(bird, let_it_fly)
很明显,我们不能让一个普通的动物去飞,因此这是符合实际的。
那么 BirdCommand 其实是比 AnimalCommand 更通用的,前者能接受更多的类型,在数学意义上是后者的超集。
这种现象和前面的协变正好相反。我们把协变的定义反过来,就可以得到逆变(contravariance)的定义:如果 SubTypeSuperType 的子类型,而 GenType[SuperType, ...]GenType[SubType, ...] 的子类型,那么泛型类型 GenType[T, ...] 对类型 T 是逆变的。

顺带一提,Callable 的返回值类型是协变的,这很容易得出:
def new_bird() -> Bird:
    return Bird()


def new_animal() -> Animal:
    return Animal()


animal: Animal = new_bird()
animal = new_animal()
bird: Bird = new_bird()
bird = new_animal()  # error:
# Expression of type "Animal" is incompatible with declared type "Bird"
#  "Animal" is incompatible with "Bird"

def animal_factory(factory: NewAnimal) -> Animal:
    return factory()


def bird_factory(factory: NewBird) -> Bird:
    return factory()


factory: NewBird = new_bird
factory = new_animal  # error:
# Expression of type "() -> Animal" is incompatible with declared type "NewBird"
#   Type "() -> Animal" is incompatible with type "NewBird"
#     Function return type "Animal" is incompatible with type "Bird"
#       "Animal" is incompatible with "Bird"

animal_factory(new_bird)
animal_factory(new_animal)
bird_factory(new_bird)
bird_factory(new_animal)  # error:
# Argument of type "() -> Animal" cannot be assigned to parameter "factory" of type "NewBird" in function "bird_factory"
#   Type "() -> Animal" is incompatible with type "NewBird"
#     Function return type "Animal" is incompatible with type "Bird"
#       "Animal" is incompatible with "Bird"

再来看看 List
birds: list[Bird] = [bird]
animals: list[Animal] = birds  # error:
# Expression of type "list[Bird]" is incompatible with declared type "list[Animal]"
#   "list[Bird]" is incompatible with "list[Animal]"
#     Type parameter "_T@list" is invariant, but "Bird" is not the same as "Animal"
#     Consider switching from "list" to "Sequence" which is covariant
居然提示 list[Bird]list[Animal] 不兼容,建议改成 Sequence[Animal]
这里的 Sequence 是不可变(immutable)类型,而 List 是可变(mutable)的。
既然 List 可变,那么下面的情况就可能发生,并且静态类型检查并不会报错:
animals.append(animal)
birds[-1].fly()  # runtime error
也就是说,birdsanimals 引用了相同的 List 对象,而往 animals 添加 animal 对象会破坏 birds 的类型约束。
所以 List 不是协变的,并且它更不可能是逆变的,这种情况下则称之为不变(invariant)。虽然看上去有点像前面提到的不可变(immutable),但其实它的意思是不相关。
同理可推断出,其他的 mutable 类型(例如 SetDict 等),也是 invariant 的。

再说一个例外:Any
anything: Any = Animal()
anything.eat()
anything.fly()  # runtime error
anything.sleep()  # runtime error

bird: Bird = anything
bird.eat()
bird.fly()  # runtime error
静态类型检查并不会对它报错,因此它可以视为既是所有类型的超集,又是所有类型的子集,同时又具有所有的属性和方法。这种既可以协变,又可以逆变的现象可以称之为双变(bivariant)。
由于使用 Any 很可能会掩盖错误,而导致运行时才暴露,因此不建议滥用。

最后回到一开始的那个例子:
第一处错误是因为 Dict 不是协变的,换成 Mapping 类型就可以了。
第二处错误则是因为如果允许这样的话,d['1'] 也可以通过静态类型检查,这会隐藏错误。

0条评论 你不来一发么↓

    想说点什么呢?