Reputation: 754
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
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:
__init__
accepts arbitrary keyword arguments, and passes any it doesn't handle itself to super().__init__
.__init__
calls super.__init__
once; properly defined, each class in the hierarchy will be reached.**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.C2
, you use keywords arguments to avoid concerns over which positional arguments are handled by which method.print
; you wouldn't need to define C2.__init__
, C1.__init__
, or M2.__init__
at all.Upvotes: 1