Python 的协变、逆变与不变
2024 5 29 07:45 PM 243次查看
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]
的子集和子类型。如果
SubType
是 SuperType
的子类型,且 GenType[SubType, ...]
也是 GenType[SuperType, ...]
的子类型,那么泛型类型 GenType[T, ...]
对类型 T
是协变(covariant)的。除了
Tuple
外,还有 Union
、FrozenSet
等类型也是协变的。可是
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)的定义:如果
SubType
是 SuperType
的子类型,而 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
也就是说,birds
和 animals
引用了相同的 List
对象,而往 animals
添加 animal
对象会破坏 birds
的类型约束。所以
List
不是协变的,并且它更不可能是逆变的,这种情况下则称之为不变(invariant)。虽然看上去有点像前面提到的不可变(immutable),但其实它的意思是不相关。同理可推断出,其他的 mutable 类型(例如
Set
、Dict
等),也是 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条评论 你不来一发么↓