Python mixin with abstract methods overrides concrete methods

Question:

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
Asked By: alexvoronov

||

Answers:

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. The ABCMeta.__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

Demo: https://ideone.com/jFMuII

Answered By: blhsing