Python泛型中缺少交集类型的解决方法?

22

我遇到了一个问题,如果使用交叉类型(目前正在讨论但尚未实现),问题将很容易解决,我想知道最干净的解决方法是什么。

当前设置和问题

我的当前设置大致对应于以下动物的ABC层次结构。有一些动物的“特征”(CanFlyCanSwim等)定义为抽象子类(虽然它们也可以被定义为mixin)。

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def name(self) -> str: ...  
    
class CanFly(Animal):
    @abstractmethod
    def fly(self) -> None: ...
    
class CanSwim(Animal):
    @abstractmethod
    def swim(self) -> None: ...

通过这个定义,我可以区分抽象和具体的动物类别:

class Bird(CanFly):
    def fly(self) -> None:
        print("flap wings")
    
class Penguin(Bird, CanSwim):
    def name(self) -> str:
        return "penguin"
    def swim(self) -> None:
        print("paddle flippers")

我还定义了一个通用类,用于抚摸特定类型的动物:
from typing import Generic, TypeVar

T = TypeVar("T", bound=Animal, contravariant=True)

class Petter(Generic[T], ABC):

    @abstractmethod
    def pet(self, a: T) -> None:
        ...

然而,就我所知,没有办法指定一个 Petter 来处理特征交集的情况:例如,对于所有既能飞又能游泳的动物。

class CanFlyAndSwim(CanFly, CanSwim):
    pass
        
class CanFlyAndSwimPetter(Petter[CanFlyAndSwim]):

    def pet(self, a: CanFlyAndSwim):
        a.name()
        a.fly()
        a.swim()
    
        
CanFlyAndSwimPetter().pet(Penguin())  # type error, as Penguin isn't a subclass of CanFlyAndSwim

我可以尝试通过坚持让Penguin明确继承自CanFlyAndSwim来解决这个问题,但这种方法无法适用于更多的功能组合。

使用协议代替?

我尝试的另一种方法是使用协议:

from typing import Protocol

class AnimalProtocol(Protocol):
    def name(self) -> str: ...

class FlyProtocol(AnimalProtocol, Protocol):
    def fly(self) -> None: ...

class SwimProtocol(AnimalProtocol, Protocol):
    def swim(self) -> None: ...

有了这些,我们确实可以定义一个有用的协议交集。将类型变量T的上界更改为AnimalProtocol后,我们可以编写:

class FlyAndSwimProtocol(FlyProtocol, SwimProtocol, Protocol):
    ...

class FlyAndSwimProtocolPetter(Petter[FlyAndSwimProtocol]):

    def pet(self, a: FlyAndSwimProtocol):
        a.name()
        a.fly()
        a.swim()
    
        
FlyAndSwimProtocolPetter().pet(Penguin())  # ok

然而,将ABC替换为协议会删除定义动物时的显式类层次结构,这对于文档和检查是否实现了所有相关方法都很有用。我们可以尝试同时保留ABC和协议,但这涉及到重复编码,除非有某种方式可以从一个中定义另一个?是否有一种简洁的解决方案?

4
(顺便说一句,我知道企鹅不能飞。这只是我能想到的第一个例子!) - Uri Granta
1
为什么我感觉我知道使用案例是什么? ;) 你尝试过组合优于继承吗?我意识到这是一个非常广泛的问题,可能不会有任何结果。 - joel
1
“这对于检查所有相关方法是否已经实现非常有用。”如果您没有在协议上实现所有方法,然后尝试在期望协议的位置使用它,类型检查器不会抱怨吗? - joel
如果您尝试使用一个未实现所有必需方法的动物与宠物交互,类型检查器将会报错。但是,如果您只是定义了一个缺少某些方法的动物,则不会出现错误:显式继承协议并不会检查您是否实现了该协议的方法(其中可能有许多方法)。 - Uri Granta
1
明确地从协议继承并不检查您是否实现了该协议的方法。这对我来说是个坏消息。看起来可能取决于类型检查器。它不完全像你想要的那样明智,但如果显式子类化协议并且将方法标记为@abstractmethod,则mypy需要实现协议方法才能实例化类。 - joel
显示剩余3条评论
2个回答

0

可能有点跑题...但我认为最好的方法是使用组合而不是继承。这将消除大量的类并使其更加简单。

from abc import ABC, abstractmethod
from dataclasses import dataclass


class AnimalFeature(ABC):
    @abstractmethod
    def action(self) -> None:
        ...


class CanFly(AnimalFeature):
    @staticmethod
    def action() -> None:
        print("flap wings")


class CanSwim(AnimalFeature):
    @staticmethod
    def action() -> None:
        print("paddle flippers")


@dataclass
class Pet:
    name: str
    features: list[AnimalFeature] | None

    def pet(self):
        print(self.name)

        for feature in self.features:
            feature.action()


peter_pet = Pet(
    name="Peter",
    features=[CanFly, CanSwim],
)
peter_pet.pet()

2
这样做不能为只接受能飞和游泳参数的函数添加注释,而这正是问题所要求的。这还使得调用函数变得更加困难(你需要在特性列表中搜索fly()而不是直接调用)。 - interjay

-1

使用继承和多个混入。Python专门支持多重继承以实现此目的。


是的,但这并不优雅。用户更喜欢组合而非继承。 - Boring Guy
1
这是一个完全符合Python风格的解决方案。 - John R
1
原始代码已经使用了多重继承。您提出的建议有何不同之处? - interjay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接