Allowing abstract classes to appear in MRO more than once

The current MRO algorithm prevents you from making hierarchies like the following:

class Abstract: ...
class B(Abstract): ...
class C(Abstract, B): ...

TypeError: Cannot create a consistent method resolution
order (MRO) for bases Abstract, B

An example of where this might occur is that the author intends that C is a subtype of B. However, C’s implementation is totally different and it will not call super().__init__(), and instead roll it’s own implementation. The author wants to reintroduce the abstract class to re-enable the abstractmethod checking and get any default method implementations from Abstract that B may have overridden.

The error occurs because the linearization algorithm assumes that a class should only appear in the MRO once. That is a reasonable assumption, since you don’t want to call __init__ on a class more than once. However, Abstract is abstract and has no __init__, so it is safe to appear in the MRO more than once (AFAICT). An appropriate MRO could be [C, A, B, A]. Monotonicity is preserved, no constructor is called more than once, and the author’s intent is realized.

Concretely, I’m proposing that the linearization algorithm allow a class to appear in an MRO more than once, if it does not have an __init__ overload. In the current C3 algorithm, when the condition of “no good next head” to the merge operation occurs, search the merge list left-to-right to see if any class does not have an __init__. The first one that is found is added to the MRO, and the algorithm continues.

EDIT: Perhaps the real issue is that class MRO and __init__ linearization should occur separately?

I very much doubt that we’ll change the linearization based on a single example use case. The MRO is used for more things than __init__. Why do you want to inherit from B anyway, if the implementation is totally different? Isn’t inheriting from Abstract sufficient?

2 Likes

Why do you want to inherit from B anyway, if the implementation is totally different? Isn’t inheriting from Abstract sufficient?

To show that C is a subtype of B; so things like isinstance(c, B) work. Class hierarchies aren’t all about implementation reuse.

I understand it’s a big change, and I can work around the limitation by just not adding Abstract into C and making sure I write C with diligence, so my problem is a small one. It just seemed like an unnecessary limitation, so I was throwing the idea out there.

Hi Kaleb,

Why don’t you swap the order of Abstract and B in your C class? This
works for me.

>>> class Abstract: pass
... 
>>> class B(Abstract): pass
... 
>>> class C(B, Abstract): pass  # instead of C(Abstract, B)
...

Well the point is that I’d like Abstract to appear in C's MRO before B. I want to show that C is a subclass of B without inheriting any of the implementation. Putting Abstract before B overloads B's implementation with the original Abstract implementations.

One option I did consider, though I don’t think it’s really a good solution, is what is “convention” and is codified in languages like Rust: no inheritance of concrete classes, only inheritance of abstract classes with a final single depth of implementation. So, the hierarchy could instead be:

class AbstractB: ...
class AbstractC(AbstractB): ...
class B(AbstractB): ...
class C(AbstractC): ...

And isinstance checks would have to change to isinstance(c, AbstractB) rather than isinstance(c, B). This prevents implementation reuse via inheritance and can get verbose, but it does solve the problem.

1 Like

If you don’t want to inherit any of B’s implementation, you probably
shouldn’t have it in C’s MRO at all. Otherwise, you could inherit
something you don’t intend to.

How about this?

>>> import abc
>>> class Abstract(abc.ABC):
...     pass
... 
>>> class B(Abstract):
...     pass
... 
>>> @B.register
... class C(Abstract):
...     pass
... 
>>> issubclass(C, B)
True
3 Likes

That looks like it does what I’d like it to… I was totally unaware of this feature, I have never seen it used before. I’m guessing the original intent was to register classes that didn’t inherit from an ABC as a subclass as a form of ad-hoc subtyping?