如何在 Python 中为类似 class 的协变可变集合使用类型提示?

How to use type-hints for a covariant mutable collection-like class in Python?

我正在尝试在 Python 中创建类似 class 的协变集合,如下所示:

from typing import Generic, TypeVar, List, cast

class Animal():
    pass

class Dog(Animal):
    pass

class Cat(Animal):
    pass

class Zoo():
    def __init__(self, items: List[Animal]):
        self._items = items.copy()  # type: List[Animal]

    def add(self, animal: Animal) -> None:
        self._items.append(animal)

    def animal_count(self) -> int:
        return len(self._items)

    def get_animals(self) -> List[Animal]:
        return self._items.copy()

class DogHouse(Zoo):
    def __init__(self, items: List[Dog]):
        self._items = items.copy()  # type: List[Dog]

    def add(self, dog: Dog) -> None:
        assert isinstance(dog, Dog)
        self._items.append(dog)

总之,我喜欢subclass Zoo,这样只取特定类型的Animal。在这种情况下,一个 DogHouse 只有 Dogs.

Mypy 使用此代码给出了两个错误:

我理解 mypy 试图警告我的内容:以下代码段在句法上是有效的,但可能会导致问题,因为 DogHouse 中突然可能出现另一种动物(猫、袋鼠...)(正式地,该代码可能违反了 Liskov 替换原则):

doghouse = DogHouse([])
doghouse._items.append(Cat())

但是,我的代码应该会处理这个问题,例如通过检查 DogHouse.add() 中的类型,使 Zoo._items(有点)私有,并使大量 copy()可变序列,因此 Zoo._items 无法修改。

有没有办法既使 DogHouse 成为 Zoo 的子 class(并受益于 Zoo 中的泛型方法),又能使用类型提示验证我的代码不会意外地让猫或其他动物偷偷溜进狗屋?

我已阅读 https://mypy.readthedocs.io/en/stable/generics.html#variance-of-generics,但无法将此建议应用到我的代码中(来自像 Python 这样的鸭子式语言,我对协方差)。


编辑:我尝试通过定义 Animal_co = TypeVar('Animal_co', bound=Animal, covariant=True) 来解决问题,但这导致 error: Cannot use a covariant type variable as a parameter. 请参阅接受的答案以获得正确答案,并解释为什么这是错误的。

您在之前的编辑中对协变类型变量所做的尝试很接近,但类型变量不应该是协变的。使其协变意味着 Zoo[Dog] 也是 Zoo[Animal],特别是,这意味着 add(self, animal: Animal_co) 可以接受任何 Animal,无论 Animal_co 绑定到什么。您正在寻找的行为确实是不变的,而不是协变的。 (您可能需要一个单独的“只读”动物园 ABC 或实际上是协变的协议。)

当你在做的时候,不要再戳父级的实现细节了:

T = TypeVar('T', bound=Animal)

class Zoo(Generic[T]):
    _items: List[T]
    def __init__(self, items: Iterable[T]):
        self._items = list(items)

    def add(self, animal: T) -> None:
        self._items.append(animal)

    def animal_count(self) -> int:
        return len(self._items)

    def get_animals(self) -> List[T]:
        return self._items.copy()

class DogHouse(Zoo[Dog]):
    def add(self, dog: Dog) -> None:
        assert isinstance(dog, Dog)
        super().add(dog)

assert 用于在运行时进行类型检查。如果您实际上并不关心运行时强制执行,则可以将 DogHouse 简化为

class DogHouse(Zoo[Dog]): pass

或完全删除它并直接使用 Zoo[Dog]