0

Earlier I had a simple single-inheritance architecture between two classes C1 and C2, which worked well:

class C1:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        print("C1")

class C2(C1):
    def __init__(self, z, *args):
        C1.__init__(self, *args)
        self.z = z
        print("C2")

c2 = C2(1, 2, 3) # prints "C1" and "C2"
print(c2.x, c2.y, c2.z) # prints 2 3 1

Now the architecture has become more complicated:

class _B1: # abstract base
    def __init__(self, x):
        self.x = x
        print("B1")
class _B2(_B1): # concrete base
    def __init__(self, z, *args):
        _B1.__init__(self, *args)
        self.z = z
        print("B2")

class _M1: # abstract mixin
    def __init__(self, y):
        self.y = y
        print("M1")
class _M2(_M1): # concrete mixin
    def __init__(self, *args):
        _M1.__init__(self, *args)
        print("M2")

class C1(_M1, _B1): # abstract composed
    def __init__(self, x, y): # old signature must not be changed
        _B1.__init__(self, x)
        _M1.__init__(self, y)
        print("C1")
class C2(_M2, _B2, C1): # concrete composed; use C1 here because isinstance(c2, C1) must still return True
    def __init__(self, z, *args): # old signature must not be changed
        C1.__init__(self, *args) # works
        _B2.__init__(self, z, x) # Problem 1a: cannot do that because x is abstracted in *args
        _M2.__init__(self, y) # Problem 1b: cannot do that because y is abstracted in *args
        # Problem 2: there seem to be "two versions" of B1 and M1 (one from C1 and one from B2 or M2, resp.), and so the constructors are each called twice
        print("C2")

# c2 = C2(1, 2, 3)
# print(c2.x, c2.y, c2.z)

As stated in the code, I cannot figure out how to pass the arguments to the constructors. Also the fact that the constructors are called twice gives me the feeling that this is a bad design; however, from an OOP perspective, I could not think of a more accurate one.

I am aware of some workarounds, but I would prefer a canonical solution. In particular, I would not want to include x and y in C2.__init__.

Remirror
  • 692
  • 5
  • 14
  • 1
    You have [diamond inheritence](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem) I would strongly reconsider your design. In general you should [prefer composition over inheritence](https://stackoverflow.com/questions/49002/prefer-composition-over-inheritance) as a class that inherits from 3 direct base classes (plus more indirect bases) smells of poor design. – Cory Kramer May 22 '20 at 19:00
  • To get around Problem 1, if you know the order in which `x` and `y` will be supplied, you could just index into `args` to find them. For example, if it is known that `x` is the first argument after `z` and `y` the second, then `x = args[0]` and `y = args[1]`. – Rohan Kaushik May 22 '20 at 19:18

1 Answers1

1

This is why super exists.

class _B1: # abstract base
    def __init__(self, x, **kwargs):
        super().__init__(**kwargs)
        self.x = x
        print("B1")


class _B2(_B1): # concrete base
    def __init__(self, z, **kwargs):
        super().__init__(**kwargs)
        self.z = z
        print("B2")


class _M1: # abstract mixin
    def __init__(self, y, **kwargs):
        super().__init__(**kwargs)
        self.y = y
        print("M1")


class _M2(_M1): # concrete mixin
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        print("M2")


class C1(_M1, _B1):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        print("C1")


class C2(_M2, _B2, C1):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        print("C2")


c2 = C2(x=1, y=2, z=3)
print(c2.x, c2.y, c2.z)

The output:

B1
M1
C1
B2
M2
C2
1 2 3

Some things to note:

  1. Every __init__ accepts arbitrary keyword arguments, and passes any it doesn't handle itself to super().__init__.
  2. Each __init__ calls super.__init__ once; properly defined, each class in the hierarchy will be reached.
  3. Done correctly, **kwargs will be empty by the time object.__init__ is called. For example, when C2.__init__ is called, its kwargs contains x=1, y=2, and z=3, all of which are passed on to M2.__init__, which passes them to B2.__init__. Because B2.__init__ declares z by name, its kwargs contains only x=1 and y=2, so those are passed on, but z is not. B1 is the class in this case that calls object.__init__, but by this time each of x, y, and z has been "consumed" by one method or another.
  4. When you actually instantiate C2, you use keywords arguments to avoid concerns over which positional arguments are handled by which method.
  5. If you removed the calls to print; you wouldn't need to define C2.__init__, C1.__init__, or M2.__init__ at all.
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Nice solution; ironically I thought, my case were too complex to use super(). However, the part with the keyword arguments in C2.__init__ and C1.__init__ is cheating: All classes who relied on the old signature (and thus did not use keyword arguments) would have to be changed now. – Remirror May 22 '20 at 19:33
  • Well, yes: you often *do* have to change what you've done wrong, regardless of *why* it was wrong. – chepner May 22 '20 at 19:51