I'm hitting an issue that would be easily solved by intersection types (currently under discussion but not yet implemented) and was wondering what the cleanest workaround is.
Current setup and problem
My current setup roughly corresponds to the following ABC hierarchy for animals. There are a number of animal 'features' (CanFly
, CanSwim
, etc) defined as abstract subclasses (though they could also have been defined as mixins).
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: ...
With this I define specific classes of animals, both abstract and concrete:
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")
I also define a generic class for petting specific types of animals:
from typing import Generic, TypeVar
T = TypeVar("T", bound=Animal, contravariant=True)
class Petter(Generic[T], ABC):
@abstractmethod
def pet(self, a: T) -> None:
...
However, there is no way that I know of to specify a Petter for an intersection of features: e.g. for all animals that can both fly and swim.
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
I could try to work around this by insisting that Penguin
explicitly inherit from CanFlyAndSwim
, but this isn't scalable to more feature combinations.
Using Protocols instead?
Another approach I tried is to use Protocols instead:
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: ...
With these we can indeed define a useful protocol intersection. After changing the type variable T
upper bound to AnimalProtocol
, we can write:
class FlyAndSwimProtocol(FlyProtocol, SwimProtocol, Protocol):
...
class FlyAndSwimProtocolPetter(Petter[FlyAndSwimProtocol]):
def pet(self, a: FlyAndSwimProtocol):
a.name()
a.fly()
a.swim()
FlyAndSwimProtocolPetter().pet(Penguin()) # ok
However, replacing the ABCs with Protocols removes the explicit class hierarchy when defining animals, which is useful both for documentation and for checking that all the relevant methods have been implemented. We could try keeping both the ABCs and the Protocols, though that involves significant code duplication, unless there's some way to define one from the other?
Is there a clean solution to all this?
@abstractmethod
– Bonbon