Python Multiple Inheritance Passing Arguments to Constructors Using Super

python multiple inheritance passing arguments to constructors using super

Well, when dealing with multiple inheritance in general, your base classes (unfortunately) should be designed for multiple inheritance. Classes B and C in your example aren't, and thus you couldn't find a proper way to apply super in D.

One of the common ways of designing your base classes for multiple inheritance, is for the middle-level base classes to accept extra args in their __init__ method, which they are not intending to use, and pass them along to their super call.

Here's one way to do it in python:

class A(object):
def __init__(self,a):
self.a=a

class B(A):
def __init__(self,b,**kw):
self.b=b
super(B,self).__init__(**kw)

class C(A):
def __init__(self,c,**kw):
self.c=c
super(C,self).__init__(**kw)

class D(B,C):
def __init__(self,a,b,c,d):
super(D,self).__init__(a=a,b=b,c=c)
self.d=d

This can be viewed as disappointing, but that's just the way it is.

How does Python's super() work with multiple inheritance?

This is detailed with a reasonable amount of detail by Guido himself in his blog post Method Resolution Order (including two earlier attempts).

In your example, Third() will call First.__init__. Python looks for each attribute in the class's parents as they are listed left to right. In this case, we are looking for __init__. So, if you define

class Third(First, Second):
...

Python will start by looking at First, and, if First doesn't have the attribute, then it will look at Second.

This situation becomes more complex when inheritance starts crossing paths (for example if First inherited from Second). Read the link above for more details, but, in a nutshell, Python will try to maintain the order in which each class appears on the inheritance list, starting with the child class itself.

So, for instance, if you had:

class First(object):
def __init__(self):
print "first"

class Second(First):
def __init__(self):
print "second"

class Third(First):
def __init__(self):
print "third"

class Fourth(Second, Third):
def __init__(self):
super(Fourth, self).__init__()
print "that's it"

the MRO would be [Fourth, Second, Third, First].

By the way: if Python cannot find a coherent method resolution order, it'll raise an exception, instead of falling back to behavior which might surprise the user.

Example of an ambiguous MRO:

class First(object):
def __init__(self):
print "first"

class Second(First):
def __init__(self):
print "second"

class Third(First, Second):
def __init__(self):
print "third"

Should Third's MRO be [First, Second] or [Second, First]? There's no obvious expectation, and Python will raise an error:

TypeError: Error when calling the metaclass bases
Cannot create a consistent method resolution order (MRO) for bases Second, First

Why do the examples above lack super() calls? The point of the examples is to show how the MRO is constructed. They are not intended to print "first\nsecond\third" or whatever. You can – and should, of course, play around with the example, add super() calls, see what happens, and gain a deeper understanding of Python's inheritance model. But my goal here is to keep it simple and show how the MRO is built. And it is built as I explained:

>>> Fourth.__mro__
(<class '__main__.Fourth'>,
<class '__main__.Second'>, <class '__main__.Third'>,
<class '__main__.First'>,
<type 'object'>)

Python Multiple Inheritance super().__init__()

This can't be done just using super() calls, because the superclass of the last class in the chain will be object, which doesn't take a my_param parameter.

See python multiple inheritance passing arguments to constructors using super for more discussion of parameter passing to __init__() methods with multiple inheritance.

So you need to change X to call the superclass init methods explicitly, rather than using super(). And A and B shouldn't call super().__init__() because they're subclasses of object, which doesn't do anything in its __init__().

class A:
def __init__(self, my_param):
self.my_param = my_param

class B:
def __init__(self, my_param):
self.my_param = my_param * 2

class X(A, B):
def __init__(self, my_param):
A.__init__(self, my_param=my_param)
B.__init__(self, my_param=my_param)

Multiple inheritance and passing arguments to inherited class in python

I changed the class defintion of D as follows and it worked

class D(B, C, A):
def __init__(self, a, b):
super(D, self).__init__(a=a, b=b)

So the overall class will look like, (I removed the methods to keep the code short)

class A(object):
def __init__(self, a, b):
self.a = a
self.b = b
self.c = ""

class B(A):
def __init__(self, **kw):
super(B, self).__init__(**kw)
self.x = 0

class C(A):
def __init__(self, **kw):
super(C, self).__init__(**kw)
self.c = ""
self.y = 1

class D(B, C, A):
def __init__(self, a, b):
super(D, self).__init__(a=a, b=b)

c = D(1, 2)
print(c.a, c.b, c.x, c.c, c.y)

The output will be

1 2 0  1

This is due to a rule in the MRO algorithm which says (More details in this answer here, but the gist is

a class always appears before its ancestor ("monotonicity")

Hence B and C needs to appear before A, since A is the ancestor of B and C

Or in other words:
D is inheriting from A,B and C. Because B and C already inherits from A, there is no way python now cannot determine what class to look methods up on first; either A, or B and C, if you use the old order of defining. D(A, B, C)

Multiple inheritance in python3 with different signatures

Do not use super(baseclass, ...) unless you know what you are doing. The first argument to super() tells it what class to skip when looking for the next method to use. E.g. super(A, ...) will look at the MRO, find A, then start looking for __init__ on the next baseclass, not A itself. For C, the MRO is (C, A, B, object), so super(A, self).__init__ will find B.__init__.

For these cases, you don't want to use cooperative inheritance but directly reference A.__init__ and B.__init__ instead. super() should only be used if the methods you are calling have the same signature or will swallow unsupported arguments with *args and **vargs. In that case just the one super(C, self).__init__() call would be needed and the next class in the MRO order would take care of chaining on the call.

Putting it differently: when you use super(), you can not know what class will be next in the MRO, so that class better support the arguments you pass to it. If that isn't the case, do not use super().

Calling the base __init__ methods directly:

class A(object):
def __init__(self, a, b):
print('Init {} with arguments {}'.format(self.__class__.__name__, (a, b)))

class B(object):
def __init__(self, q):
print('Init {} with arguments {}'.format(self.__class__.__name__, (q)))

class C(A, B):
def __init__(self):
# Unbound functions, so pass in self explicitly
A.__init__(self, 1, 2)
B.__init__(self, 3)

Using cooperative super():

class A(object):
def __init__(self, a=None, b=None, *args, **kwargs):
super().__init__(*args, **kwargs)
print('Init {} with arguments {}'.format(self.__class__.__name__, (a, b)))

class B(object):
def __init__(self, q=None, *args, **kwargs):
super().__init__(*args, **kwargs)
print('Init {} with arguments {}'.format(self.__class__.__name__, (q)))

class C(A, B):
def __init__(self):
super().__init__(a=1, b=2, q=3)

Multiple Inheritance with kwargs

Since the method resolution order is (__main__.C, __main__.A, __main__.B, object), could class B be defined in the following way instead?

No, because then this would fail:

class D:
def __init__(self, d, **kwargs):
self.d = d
super().__init__(**kwargs)

class E(C, D):
def __init__(self, e, **kwargs):
self.e = e
super().__init__(**kwargs)

The MRO of E is (E, C, A, B, D, object), so B must call super().__init__ otherwise D.__init__ won't be called.

Isn't super().__init__(**kwargs) in class B redundant, since any surplus kwargs passed to C will be passed to object, raising?

No, because the surplus kwargs will go to D.__init__ in the above example. But even without that, it is not redundant to raise an error when you call a constructor with too many arguments; it is desirable to have an error message informing you about your incorrect code, rather than for the mistake to go undetected.

Is this a safeguard for if C was defined as class C(B, A) instead of class C(A, B)?

In some sense, sure; but really it's a safeguard for B occurring in any class hierarchy, so long as the other classes in the hierarchy follow the same rule of calling super().__init__(**kwargs).

Multiple Inheritance and calling super()

First, you have to look at the method resolution order of Top:

>>> for c in Top.__mro__: print c
...
<class '__main__.Top'>
<class '__main__.Middle1'>
<class '__main__.Middle2'>
<class '__main__.Middle3'>
<class '__main__.Base'>
<type 'object'>

This helps you see which class each call to super represents.

Your mistake is thinking that the call to super(Middle1, self) refers to the (only) base class Base of Middle1. It does not: it refers to the the class following Middle1 in the MRO of self.__class__. Since self.__class__ is Top, the next class in line is Middle2, whose __init__ takes only one argument.

To use super correctly from a method, you need to ensure that the method takes the same arguments in every class, because you cannot predict which class's method will be called by looking at the code itself; it depends entirely on the type of the object that initiates the chain of calls, which might be a class you aren't even aware of yet.

There are two posts I suggest reading:

  • Python's super() considered super!
  • Python's Super is nifty, but you can't use it very carefully to understand the semantics of super.

Together, they give you a good understanding of when super can be used correctly and how to avoid the problem you see here.

(In full disclosure, I haven't read either post recently, so I will refrain from trying to summarize the advice presented in each.)

How to pass arguments to parent class with inheritance and with super keyword?

The point of inheritance is to indicate that a certain class (ex. Dog) has an "is a" relationship with a parent class (ex. Animal). So, a Dog is an Animal but with certain properties that only dogs have (or appropriate only to dogs).

class Animal:
def __init__(self, name):
self.name = name

class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed

a_dog = Dog("woolfie", "labrador")

Here, the __init__ still takes a name, but also takes in extra attributes specific for a dog, such as breed. For the common attribute name, that's where you can just pass it along to the parent class with super.


So, applying that to your code, I'm assuming a BioData_OP object is a type of BioData, but with added age and address attributes. The __init__ should probably still accept the same first name and last name, but then just have added parameters for age and address.

class BioData:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name

class BioData_OP(BioData):
def __init__(self, first_name, last_name, age, address):
super().__init__(first_name, last_name)
self.age = age
self.address = address

mydata = BioData_OP("Big","Bee", 36,"CNGF")

As recommended in the comments, depending on your intended use-case or situation, if this going to be part of a factory design pattern, you should make sure not to change the base class' initialization, but instead, let your subclass accept optional values for subclass-specific values or provide a separate method for setting those values.

class BioData:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name

class BioData_OP(BioData):
def __init__(self, first_name, last_name, age=-1, address=None):
super().__init__(first_name, last_name)
self.age = age
self.address = address

def update(self, age, address):
self.age = age
self.address = address

# Still works
# Will create a BioData_OP object but without age and address
# Basically creates a BioData object
mydata_1 = BioData_OP("Big","Bee")

# Directly create a BioData_OP object
mydata_2 = BioData_OP("Big","Bee", 36,"CNGF")

# First, create a BioData object
# Then, turn in into a BioData_OP object
mydata_3 = BioData_OP("Big","Bee")
mydata_3.update(36, "CNGF")


Related Topics



Leave a reply



Submit