Remirror
Remirror

Reputation: 754

Simultaneous multiple inheritance both in composed class and its components (Python)

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

Upvotes: 0

Views: 60

Answers (1)

chepner
chepner

Reputation: 531605

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.

Upvotes: 1

Related Questions