A note on multiple inheritance in Python

July 2, 2022

Once I was asked at an interview (which I seemed to fail because they did not contact me anymore): what's special in multiple inheritance in Python? Well, such a question is somewhat weird. What special? Special implies among others, or in comparison with others. Which others? C++? I barely remember the details, I need to open the ABC book to recall. So, nothing special. That's my answer. Inheritance as inheritance. Multiple as multiple. The only thing we have to worry about is MRO, method resolution order, which also applies to attributes:

class A:
    x = 1

class B:
    x = 2

class C(A,B):
    pass

print(C().x)
>>> 1
class D(B,A):
    pass

print(D().x)
>>> 2
101+1

Basically, I try to avoid multiple inheritance whenever possible, but in a very few cases it's really useful. My Clabate library uses multiple inheritance in its markup module to define MarkupTemplate and MinifiedMarkupTemplate. In my previous blog post I briefly mentioned this without diving into details.

In Clabate, markup template classes extend basic templates with the same set of basic methods. However, base classes are different: MarkupTemplate is based on Template class which maintains indentation. MinifiedMarkupTemplate is based on LeanTemplate which is faster and indentation does not make sense:

class MarkupTemplate(Template):

    def get_missing_value(self, name, context):
        # ...

    @classmethod
    def dedent(self, text):
        # ...

class MinifiedMarkupTemplate(LeanTemplate)
    # same methods as in MarkupTemplate:

    def get_missing_value(self, name, context):
        # ...

    @classmethod
    def dedent(self, text):
        # ...

So, how would we do that? Copy-paste the implementation? That's not a good way. Instead, a common base class, MarkupTemplateBase, is defined. As long as it overrides some base methods, it goes first in class definitions:

class MarkupTemplateBase:
    # common methods

    def get_missing_value(self, name, context):
        # ...

    @classmethod
    def dedent(self, text):
        # ...

class MarkupTemplate(MarkupTemplateBase, Template):

    def create_formatter(self):
        # use MarkupFormatter
        # ...

class MinifiedMarkupTemplate(MarkupTemplateBase, LeanTemplate)

    def create_formatter(self):
        # use LeanMarkupFormatter
        # ...

    # one more method to amend:
    def format_string(self, s, context):
        # ...

By the way, this idea comes from Desige, declassed site generator, which allows replacing the basic HTML boilerplate, along with any other base classes. But it does that without multiple inheritance.

Recently I tried to improve the structure of Desige. That's my favorite toy and I write it running in circles, from chaos to order and back from order to chaos. At some point I realized that I need to extend attributes of base classes, for example, to append some styles and javascript code for particular templates. Initially I used cumbersome template strings for that purpose. For some reason I overlooked the fact that I could simply redefine an attribute with a property and use super() to get that attribute from the base class. I mean, the following does work:

class A:
    x = 1

class B:
    @property
    def x(self):
        return super().x + 1

class C(B,A):
    pass

print(C().x)
>>> 2

However a real code from my Clabate template looks way too cumbersome. It's like a dirty palette:

@property
def embedded_scripts(self, context):
    return super().embedded_scripts + ['asoke.js']

What if I had something similar to super? Like this:

embedded_scripts = superattr().embedded_scripts + ['asoke.js']

I don't know about you, but I think this could be truly artistic brush strokes on the masterpiece.

Similar to properties, which are dynamic, that seems possible. Of course, if the underlying implementation will be nice. Otherwise I wouldn't bother. It's not worth to make a candy from shit.

What we need? My initial thought, based on super, was as follows: we need superattr() function which would return an object, whose __getattr__ method would return a descriptor for the requested attribute. That descriptor would call

super().getattr('embedded_scripts')

and return it's value. Possible? Surely!

However, descriptors make things much easier and more artistic:

print('Defining...')

class superattr:
    '''
    This is a descriptor that allows extending attributes in a simple and elegant way:

        my_attr = superattr() + some_addition_to_my_attr
    '''
    def __init__(self):
        self.additions = []

    def __set_name__(self, owner, name):
        print('__set_name__', name)
        self.attr_name = name

    def __get__(self, obj, objtype=None):
        for cls in obj.__class__.__mro__[1:]:
            try:
                value = getattr(cls, self.attr_name)
            except AttributeError:
                continue
            for a in self.additions:
                value = value + a
            return value
        raise AttributeError(self.attr_name)

    def __add__(self, other):
        print('__add__:', other)
        self.additions.append(other)
        return self

from clabate import Markup  # just to show that Markup works seamlessly with such an approach

class A:
    x = 1
    html = Markup('<div>Some HTML</div>')
    embedded_scripts = ['base.js']

class B(A):
    x = superattr() + 1
    html = superattr() + Markup('<div>More HTML</div>')
    embedded_scripts = superattr() + ['comments.js']

print('Instantiating...')
b = B()
print('Running...')
print(b.x)
print(type(b.html), b.html)
print(b.embedded_scripts)

Although it does not seem possible to call super from a descriptor (haven't even tried that after re-reading the doc), it's not much difficult to handle __mro__. And this works seamlessly with Markup, because Markup implements __add__ method.