Two mixin classes specify requirements as abstract methods. Together, the classes have a full set of concrete methods. However, they fail to combine into a concrete class: no matter which order I use to declare the concrete class, some abstract methods override the concrete ones.
Is there a way to prevent abstract methods from overriding the concrete methods? I believe this works in Scala for example.
What are alternative ways to specify the requirements?
Here is a concrete example:
import abc
class A(abc.ABC):
@abc.abstractmethod
def x(self):
pass
def y(self):
return "A.y"
class B(abc.ABC):
@abc.abstractmethod
def y(self):
pass
def x(self):
return f"B.x"
class AB(B, A):
pass
class BA(A, B):
pass
ab = AB() # TypeError: Can't instantiate abstract class AB with abstract methods y
ba = BA() # TypeError: Can't instantiate abstract class BA with abstract methods x
The problem here is that when abstract methods are calculated for a subclass of one or more abstract classes, only the subclass itself, rather than all the bases, is looked up upon to determine if an abstract method has been defined concretely.
Since we know from PEP-3119 that the names of the abstract methods of an abstract class are stored in the __abstractmethods__
attribute, and that only an abstract method has the __isabstractmethod__
attribute set to True
:
The
@abstractmethod
decorator sets the function attribute__isabstractmethod__
to the value True. TheABCMeta.__new__
method computes the type attribute__abstractmethods__
as the set of all method names that have an__isabstractmethod__
attribute whose value is true.
we can override ABCMeta.__new__
with additional logics after instantiating the class to check if methods that are deemed to have remained abstract by the original calculations can actually be found to be defined concretely in any of the base classes, in which case define references to them in the subclass, and remove them from __abstractmethods__
of the subclass:
class ComprehensiveABCMeta(abc.ABCMeta):
_NOTFOUND = object()
def __new__(metacls, name, bases, namespace, /, **kwargs):
cls = super().__new__(metacls, name, bases, namespace, **kwargs)
abstracts = set(cls.__abstractmethods__)
for name in cls.__abstractmethods__:
for base in bases:
value = getattr(base, name, cls._NOTFOUND)
if not (value is cls._NOTFOUND or
getattr(value, '__isabstractmethod__', False)):
setattr(cls, name, value)
abstracts.remove(name)
break
cls.__abstractmethods__ = frozenset(abstracts)
return cls
class ComprehensiveABC(metaclass=ComprehensiveABCMeta):
pass
so that:
class A(ComprehensiveABC):
@abc.abstractmethod
def x(self):
pass
def y(self):
return "A.y"
class B(ComprehensiveABC):
@abc.abstractmethod
def y(self):
pass
def x(self):
return "B.x"
class AB(B, A):
pass
ab = AB()
print(ab.x())
print(ab.y())
outputs:
B.x
A.y