pythonmultiple-inheritancemixinspython-dataclassesmethod-resolution-order

Achieving multiple inheritance using python dataclasses


I'm trying to use the new python dataclasses to create some mix-in classes (already as I write this I think it sounds like a rash idea), and I'm having some issues. Behold the example below:

from dataclasses import dataclass

@dataclass
class NamedObj:
    name: str
            
    def __post_init__(self):
        print("NamedObj __post_init__")
        self.name = "Name: " + self.name
    
@dataclass
class NumberedObj:
    number: int = 0
        
    def __post_init__(self):
        print("NumberedObj __post_init__")
        self.number += 1
    
@dataclass
class NamedAndNumbered(NumberedObj, NamedObj):
    
    def __post_init__(self):
        super().__post_init__()
        print("NamedAndNumbered __post_init__")

If I then try:

nandn = NamedAndNumbered('n_and_n')
print(nandn.name)
print(nandn.number)

I get

NumberedObj __post_init__
NamedAndNumbered __post_init__
n_and_n
1

Suggesting it has run __post_init__ for NamedObj, but not for NumberedObj. What I would like is to have NamedAndNumbered run __post_init__ for both of its mix-in classes, Named and Numbered. One might think that it could be done if NamedAndNumbered had a __post_init__ like this:

def __post_init__(self):
    super(NamedObj, self).__post_init__()
    super(NumberedObj, self).__post_init__()
    print("NamedAndNumbered __post_init__")

But this just gives me an error AttributeError: 'super' object has no attribute '__post_init__' when I try to call NamedObj.__post_init__().

At this point I'm not entirely sure if this is a bug/feature with dataclasses or something to do with my probably-flawed understanding of Python's approach to inheritance. Could anyone lend a hand?


Solution

  • This:

    def __post_init__(self):
        super(NamedObj, self).__post_init__()
        super(NumberedObj, self).__post_init__()
        print("NamedAndNumbered __post_init__")
    

    doesn't do what you think it does. super(cls, obj) will return a proxy to the class after cls in type(obj).__mro__ - so, in your case, to object. And the whole point of cooperative super() calls is to avoid having to explicitely call each of the parents.

    The way cooperative super() calls are intended to work is, well, by being "cooperative" - IOW, everyone in the mro is supposed to relay the call to the next class (actually, the super name is a rather sad choice, as it's not about calling "the super class", but about "calling the next class in the mro").

    IOW, you want each of your "composable" dataclasses (which are not mixins - mixins only have behaviour) to relay the call, so you can compose them in any order. A first naive implementation would look like:

    @dataclass
    class NamedObj:
        name: str
    
        def __post_init__(self):
            super().__post_init__()
            print("NamedObj __post_init__")
            self.name = "Name: " + self.name
    
    @dataclass
    class NumberedObj:
        number: int = 0
    
        def __post_init__(self):
            super().__post_init__()
            print("NumberedObj __post_init__")
            self.number += 1
    
    @dataclass
    class NamedAndNumbered(NumberedObj, NamedObj):
    
        def __post_init__(self):
            super().__post_init__()
            print("NamedAndNumbered __post_init__")
    

    BUT this doesn't work, since for the last class in the mro (here NamedObj), the next class in the mro is the builtin object class, which doesn't have a __post_init__ method. The solution is simple: just add a base class that defines this method as a noop, and make all your composable dataclasses inherit from it:

    class Base(object):
        def __post_init__(self):
            # just intercept the __post_init__ calls so they
            # aren't relayed to `object`
            pass
    
    @dataclass
    class NamedObj(Base):
        name: str
    
        def __post_init__(self):
            super().__post_init__()
            print("NamedObj __post_init__")
            self.name = "Name: " + self.name
    
    @dataclass
    class NumberedObj(Base):
        number: int = 0
    
        def __post_init__(self):
            super().__post_init__()
            print("NumberedObj __post_init__")
            self.number += 1
    
    @dataclass
    class NamedAndNumbered(NumberedObj, NamedObj):
    
        def __post_init__(self):
            super().__post_init__()
            print("NamedAndNumbered __post_init__")