Understanding super in Python - python

Could you please explain to me how to write the deadly diamond in Python? I saw many examples of similar code without using constructor arguments, but once I start using arguments thigs start being messy...
class A:
def __init__(self, a):
self.a = a
class B(A):
def __init__(self, a, b):
self.b = b
super().__init__(a)
class C(A):
def __init__(self, a, c):
self.c = c
super().__init__(a)
class D(B, C):
def __init__(self, a, b, c, d):
self.d = d
# How do I pass a and b to B.__init__
# and a and c to C.__init__
# using super() ?
super().__init__(a, b, c) #???
d = D(1, 2, 3, 4)

The simplest way would probably be that every subclass accepts a dict of kwargs, and passes it to the upper level:
class A:
def __init__(self, a):
self.a = a
class B(A):
def __init__(self, b, **kwargs):
self.b = b
super().__init__(**kwargs)
class C(A):
def __init__(self, c, **kwargs):
self.c = c
super().__init__(**kwargs)
class D(B, C):
def __init__(self, d, **kwargs):
self.d = d
super().__init__(**kwargs)
d = D(a=1, b=2, c=3, d=4)
print(d.a, d.b, d.c, d.d)
# 1 2 3 4
At each call, __init__ gets the argument it requires, and passes the remaining ones to the parent class. The only drawback is that you have to pass the arguments as keywords.

Your subclasses initializers don't have compatible signatures, which is the key to proper cooperative super calls.
The simple solution here is to make those signatures compatible - canonically by adding support for varargs and arbitrary keyword args all along the chain, ie:
# NB: Python3 required, won't work in Py2
class A:
def __init__(self, a, *args, **kwargs):
self.a = a
class B(A):
def __init__(self, a, b, *args, **kwargs):
super().__init__(a, *args, **kwargs)
self.b = b
class C(A):
def __init__(self, a, c, *args, **kwargs):
super().__init__(a, *args, **kwargs)
self.c = c
class D(B, C):
def __init__(self, a, b, c, d, *args, **kwargs):
super().__init__(a, b=b, c=c, *args, **kwargs)
self.d = d
d = D(1, 2, 3, 4)
This will of course not solve each and every possible issue with multiple inheritance and cooperative super calls, but MI is tricky, and there are restrictions to what's possible anyway.

Related

How to minimize redundancy in passing kwargs to multiple super classes?

Given the following classes A, B, and C:
class A:
def __init__(self, a, aa, aaa):
self.a = a
self.aa = aa
self.aaa = aaa
class B:
def __init__(self, b, bb, bbb):
self.b = b
self.bb = bb
self.bbb = bbb
class C(A, B):
def __init__(self, **kwargs):
super(C, self).__init__(**kwargs)
I want to avoid having to repeat all the superclasses parameters a, aa, aaa, b, bb, bbb, in C definition:
class C(A, B):
def __init__(self, a, aa, aaa, b, bb, bbb):
super(C, self).__init__(**kwargs)
and somehow pass A and B kwargs to be resolved in super().__init__ call but this is not possible using the way I described and will result in an error:
>>> c = C(a=1, aa=2, aaa=3, b=4, bb=5, bbb=6)
TypeError: A.__init__() got an unexpected keyword argument 'b'
The correct way of doing so is calling A.__init__(self, **a_kwargs) and B.__init__(self, **b_kwargs) but as I said this creates redundant parameters I'm trying to avoid. Is there a better way to achieve the same thing?
You can do this:
class A:
def __init__(self, a, aa, aaa, **kwargs):
self.a = a
self.aa = aa
self.aaa = aaa
super().__init__(**kwargs)
class B:
def __init__(self, b, bb, bbb, **kwargs):
self.b = b
self.bb = bb
self.bbb = bbb
super().__init__(**kwargs)
class C(A, B):
pass
c = C(a=1, aa=2, aaa=3, b=4, bb=5, bbb=6)
Since the C's MRO is [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>], A will consume it's parameters and pass the rest to B.
If you need to pass positional arguments too, You can change it to:
class A:
def __init__(self, a, aa, aaa, *args, **kwargs):
self.a = a
self.aa = aa
self.aaa = aaa
super().__init__(*args, **kwargs)
class B:
def __init__(self, b, bb, bbb, *args, **kwargs):
self.b = b
self.bb = bb
self.bbb = bbb
super().__init__(*args, **kwargs)
class C(A, B):
pass
You could use built-in signature(callable) function to obtain expected keyword arguments for each __init__ and pass only those that the function expects like so. Whether this is good design, that is a matter of another discussion.
from inspect import signature
class A:
def __init__(self, a, aa, aaa):
self.a = a
self.aa = aa
self.aaa = aaa
print(f"A obj: {a=}, {aa=}, {aaa=}")
class B:
def __init__(self, b, bb, bbb):
self.b = b
self.bb = bb
self.bbb = bbb
print(f"B obj: {b=}, {bb=}, {bbb=}")
class C(A, B):
def __init__(self, **kwargs):
A_params = {k:v for k, v in kwargs.items() if k in signature(A.__init__).parameters.keys()}
A.__init__(self, **A_params)
B_params = {k:v for k, v in kwargs.items() if k in signature(B.__init__).parameters.keys()}
B.__init__(self, **B_params)
c = C(a=1, aa=2, aaa=3, b=4, bb=5, bbb=6)
Output:
A obj: a=1, aa=2, aaa=3
B obj: b=4, bb=5, bbb=6

Best way of solving diamond problem in Python with fields

Python solves the diamond problem well if there are no fields in the classes by linearizing the method resolution order. However, if the classes have fields then how do you call the super constructors? Consider:
class A:
def __init__(self, a):
self.a = a # Should only be initialized once.
class B(A):
def __init__(self, a, b):
super().__init__(a)
self.b = b
class C(A):
def __init__(self, a, c, b=None):
super().__init__(a)
self.c = c
class D(C, B):
def __init__(self, a, b, c):
super().??? # What do you put in here.
For my use case I do actually have a solution, because b can't be None in the application and therefore the following largely works:
class A:
def __init__(self, a):
self.a = a # Should only be initialized once.
class B(A):
def __init__(self, a, b):
assert b is not None # Special case of `b` can't be `None`.
super().__init__(a)
self.b = b
class C(A):
def __init__(self, a, c, b=None): # Special init with default sentinel `b`.
if b is None:
super().__init__(a) # Normally `C`'s super is `A`.
else:
super().__init__(a, b) # From `D` though, `C`'s super is `B`.
self.c = c
class D(C, B): # Note order, `C`'s init is super init.
def __init__(self, a, b, c):
super().__init__(a, c, b)
def main():
A('a')
B('b', 1)
C('c', 2)
D('d', 3, 4)
C('c2', 5, 6) # TypeError: __init__() takes 2 positional arguments but 3 were given
This largely works for the special case of b can't be None, however it still has a problem if C's __init__ is called directly (see last line of above). Also you have to modify C for the multiple inheritance and you have to inherit in the order C, B.
==== Edit ===
Another possibility is to manually initialize each field (this is somewhat similar to how Scala handles fields under the covers).
class A0:
def __init__(self, a): # Special separate init of `a`.
self._init_a(a)
def _init_a(self, a):
self.a = a
class B0(A0):
def __init__(self, a, b): # Special separate init of `b`.
self._init_a(a)
self._init_b(b)
def _init_b(self, b):
self.b = b
class C0(A0):
def __init__(self, a, c): # Special separate init of `c`.
self._init_a(a)
self._init_c(c)
def _init_c(self, c):
self.c = c
class D0(C0, B0):
def __init__(self, a, b, c): # Uses special separate inits of `a`, `b`, and `c`.
self._init_a(a)
self._init_b(b)
self._init_c(c)
The disadvantage of this approach is that it is very non-standard, to the extent that PyCharm gives a warning about not calling super init.
==== End edit ===
Is there a better way?
Thanks in advance for any help, Howard.

TypeError: python multiple inheritance with different arguments

I'm trying to use multiple inheritance to add some functionality to one of the existing classes that I have. The problem is that this new class and my current base class have different arguments in their constructors. Namely the new class has 1 extra argument. After some googling I understood that I can add **kwargs to the current base class(the one with one argument less). Example:
class A(object):
def __init__(self, a):
print('__init__', locals())
class B(A):
def __init__(self, a, b):
super(B, self).__init__(a)
print('__init__', locals())
class C(B):
def __init__(self, a, b):
super(C, self).__init__(a, b)
print('__init__', locals())
class D(C):
def __init__(self, a, b):
super(D, self).__init__(a, b)
print('__init__', locals())
class E(D):
def __init__(self, a, b, *args, **kwargs):
super(E, self).__init__(a, b)
print('__init__', locals())
class F(C):
def __init__(self, a, b):
super(F, self).__init__(a, b)
print('__init__', locals())
class G(F):
def __init__(self, a, b, c):
super(G, self).__init__(a, b)
print('__init__', locals())
class H(G):
def __init__(self, a, b, c):
super(H, self).__init__(a, b, c)
print('__init__', locals())
class I(E, H):
def __init__(self, a, b, c):
super(I, self).__init__(a, b, c=c)
print('__init__', locals())
for c in I.__mro__:
print(c)
I(0, 1, 2)
But I get this error:
<class '__main__.I'>
<class '__main__.E'>
<class '__main__.D'>
<class '__main__.H'>
<class '__main__.G'>
<class '__main__.F'>
<class '__main__.C'>
<class '__main__.B'>
<class '__main__.A'>
<class 'object'>
Traceback (most recent call last):
File "/tmp/c.py", line 58, in <module>
I(0,1,2)
File "/tmp/c.py", line 50, in __init__
super(I, self).__init__(a, b, c=c)
File "/tmp/c.py", line 26, in __init__
super(E, self).__init__(a, b)
File "/tmp/c.py", line 20, in __init__
super(D, self).__init__(a, b)
TypeError: __init__() missing 1 required positional argument: 'c'
This code work for me!!!
class A(object):
def __init__(self, a):
print("A class")
class B(A):
def __init__(self, a, b):
A.__init__(self,a)
print("B class")
class C(B):
def __init__(self, a, b):
B.__init__(self,a, b)
print("C class")
class D(C):
def __init__(self, a,b):
C.__init__(self,a, b)
print("D class")
class F(C):
def __init__(self, a,b):
#C.__init__(self,a, b)
print("F class")
class G(F):
def __init__(self, a, b, c):
F.__init__(self,a, b)
print("G class")
class E(D):
def __init__(self, a, b):
D.__init__(self,a, b)
print("E class")
class H(G):
def __init__(self, a,b,c):
G.__init__(self,a, b, c)
print("H class")
class I(E,H):
def __init__(self, a, b, c):
args=(a,b,c)
E.__init__(self,a,b)
H.__init__(self,a,b,c)
print('__init__', locals())
print(I.__mro__)
I(1,2,3)
As per the MRO, the call goes to H after D, and so, if you need to send c, class D will need to accept it and send 3 parameters, ie. H will be called by D.
E.G.:
class A(object):
def __init__(self, a):
print('a')
print('__init__', locals())
class B(A):
def __init__(self, a, b):
print('b')
super(B, self).__init__(a)
print('__init__', locals())
class C(B):
def __init__(self, a, b):
print('c')
super(C, self).__init__(a, b)
print('__init__', locals())
class D(C):
def __init__(self, a, b, *args, **kwargs):
print('d', args, kwargs)
super(D, self).__init__(a, b, args, kwargs)
print('__init__', locals())
class E(D):
def __init__(self, a, b, *args, **kwargs):
print('e', args, kwargs)
super(E, self).__init__(a, b)
print('__init__', locals())
class F(C):
def __init__(self, a, b):
print('f')
super(F, self).__init__(a, b)
print('__init__', locals())
class G(F):
def __init__(self, a, b, c):
print('g')
super(G, self).__init__(a, b)
print('__init__', locals())
class H(G):
def __init__(self, a, b, c, *args, **kwargs):
print('h')
super(H, self).__init__(a, b, c)
print('__init__', locals())
class I(E,H):
def __init__(self, a, b, c):
print('i')
super(I,self).__init__(a, b, c)
#E.__init__(self,a, b)
#H.__init__(self,a, b, c)
print('__init__', locals())
for c in I.__mro__:
print(c)
I(0, 1, 2)
This code works,
(I have changed c as an arg instead of **kwarg).
Other way would be if you swap the E, H inheritance order, the MRO works out and you won't need to do it, or use E.__init__() and H.__init__() separately. In that case, MRO changes again, and common classes will be called twice if needed.
For MRO, I have found this answer and this blog post by Guido Van Rossum (also linked in the other answer) which might help you get insight on the algorithm for MRO in python.
It's difficult to know what's the business case you're trying to deal with, and what drives this specific form of inheritance. That being said, you're getting the exception because the class 'I' first calls init of 'E', and only then of 'H'.
The easiest way to solve the issue would be to switch the order of I's base-classes to what seems more natural in your case:
class I(H, E): # original code was I(E, H)
def __init__(self, a, b, c):
super(I, self).__init__(a, b, c=c)
print('__init__', locals())
This solves the issue.

Passing arguments to superclass constructor without repeating them in childclass constructor

class P(object):
def __init__(self, a, b):
self.a = a
self.b = b
class C(P):
def __init__(self, c):
P.__init__()
self.c = c
obj = C(a, b, c) #want to instantiate a C with something like this
I want to define C class object without rewriting all the P class constructor argument in C's constructor, but the above code doesn't seem to work. What is the right approach to do this?
Clarification:
The idea is to avoid putting parent class's constructor arguments in child class's constructor. It's just repeating too much. All my parent and child classes have many arguments to take in for constructors, so repeating them again and again is not very productive and difficult to maintain. I'm trying to see if I can only define what's unique for the child class in its constructor, but still initialize inherited attributes.
In Python2, you write
class C(P):
def __init__(self, a, b, c):
super(C, self).__init__(a, b)
self.c = c
where the first argument to super is the child class and the second argument is the instance of the object which you want to have a reference to as an instance of its parent class.
In Python 3, super has superpowers and you can write
class C(P):
def __init__(self, a, b, c):
super().__init__(a, b)
self.c = c
Demo:
obj = C(1, 2, 3)
print(obj.a, obj.b, obj.c) # 1 2 3
Response to your comment:
You could achieve that effect with the *args or **kwargs syntax, for example:
class C(P):
def __init__(self, c, *args):
super(C, self).__init__(*args)
self.c = c
obj = C(3, 1, 2)
print(obj.a, obj.b, obj.c) # 1 2 3
or
class C(P):
def __init__(self, c, **kwargs):
super(C, self).__init__(**kwargs)
self.c = c
obj = C(3, a=1, b=2)
print(obj.a, obj.b, obj.c) # 1 2 3
obj = C(a=1, b=2, c=3)
print(obj.a, obj.b, obj.c) # 1 2 3
You can call parent class constructor by passing self and required arguments
class C(P):
def __init__(self, a,b,c):
P.__init__(self,a,b)
self.c = c

Super with arguments in case of multiple inheritance

I have a class that inherits from two other classes whose __init__ take both a parameter like this:
class A(object):
def __init__(self, a):
self.a = a
class B(object):
def __init__(self, b):
self.b = b
class C(A, B):
def __init__(self, a, b):
super(C, self).__init__(a, b)
c = ClassC(1, 2)
This gives a TypeError: __init__() takes exactly 2 arguments (3 given).
When setting b in B to a fixed value and passing only 1 parameter to super then trying to access b in C gives an AttributeError: 'ClassC' object has no attribute 'b':
class A(object):
def __init__(self, a):
self.a = a
class B(object):
def __init__(self, b):
self.b = 2
class C(A, B):
def __init__(self, a, b):
super(C, self).__init__(a)
print self.a
print self.b
c = ClassC(1, 2)
When calling the __init__ manually everything seems to be fine:
class A(object):
def __init__(self, a):
self.a = a
class B(object):
def __init__(self, b):
self.b = b
class C(A, B):
def __init__(self, a, b):
A.__init__(a)
B.__init__(b)
print self.a
print self.b
c = ClassC(1, 2)
So how can I get this inheritance straight and how can I manage the parameters for __init__ of inherited classes when using super? Is it even possible? How does super know which parameters are to pass to which class?

Categories