Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python Multiple Inheritance: call super on all

Tags:

python

I have the following two superclasses:

class Parent1(object):
    def on_start(self):
        print('do something')

class Parent2(object):
    def on_start(self):
        print('do something else')

I would like to have a child class that inherits from both be able to call super for both parents.

class Child(Parent1, Parent2):
    def on_start(self):
        # super call on both parents

What is the Pythonic way to do this? Thanks.

like image 709
i_trope Avatar asked May 20 '15 15:05

i_trope


Video Answer


1 Answers

Exec summary:

Super only executes one method based on the class hierarchy's __mro__. If you want to execute more than one method by the same name, your parent classes need to written to cooperatively do that (by calling super implicitly or explicitly) or you need to loop over __bases__ or the __mro__ values of the child classes.

The job of super is to delegate part or all of a method call to some existing method in the classes ancestor tree. The delegation may go well outside of classes that you control. The method name delegated needs to exist in the group of base classes.

The method presented below using __bases__ with try/except is closest to a complete answer to your question of how to call each parent's method of the same name.


super is useful in the situation where you want to call one of your parent's methods, but you don't know which parent:

class Parent1(object):
    pass

class Parent2(object):
    # if Parent 2 had on_start - it would be called instead 
    # because Parent 2 is left of Parent 3 in definition of Child class
    pass

class Parent3(object):
    def on_start(self):
        print('the ONLY class that has on_start')        

class Child(Parent1, Parent2, Parent3):
    def on_start(self):
        super(Child, self).on_start()

In this case, Child has three immediate parents. Only one, Parent3, has an on_start method. Calling super resolves that only Parent3 has on_start and that is the method that is called.

If Child inherits from more than one class that has an on_start method, the order is resolved left to right (as listed in the class definition) and bottom to top (as logical inheritance). Only one of the methods is called and the other methods of the same name in the hierarchy of classes have been superseded.

So, more commonly:

class GreatGrandParent(object):
    pass

class GrandParent(GreatGrandParent):
    def on_start(self):
        print('the ONLY class that has on_start')

class Parent(GrandParent):
    # if Parent had on_start, it would be used instead
    pass        

class Child(Parent):
    def on_start(self):
        super(Child, self).on_start()

If you want to call multiple parents methods by method name, you can use __bases__ instead of super in this case and iterate over the base classes of Child without knowing the classes by name:

class Parent1(object):
    def on_start(self):
        print('do something')

class Parent2(object):
    def on_start(self):
        print('do something else')

class Child(Parent1, Parent2):
    def on_start(self):
        for base in Child.__bases__:
            base.on_start(self)

>>> Child().on_start()
do something
do something else

If there is a possibility one of the base classes does not have on_start you can use try/except:

class Parent1(object):
    def on_start(self):
        print('do something')

class Parent2(object):
    def on_start(self):
        print('do something else')

class Parent3(object):
    pass        

class Child(Parent1, Parent2, Parent3):
    def on_start(self):
        for base in Child.__bases__:
            try:
                base.on_start(self)
            except AttributeError:
                # handle that one of those does not have that method
                print('"{}" does not have an "on_start"'.format(base.__name__))

>>> Child().on_start()
do something
do something else
"Parent3" does not have an "on_start"

Using __bases__ will act similar to super but for each class hierarchy defined in the Child definition. ie, it will go though each forbearer class until on_start is satisfied once for each parent of the class:

class GGP1(object):
    def on_start(self):
        print('GGP1 do something')

class GP1(GGP1):
    def on_start(self):
        print('GP1 do something else')

class Parent1(GP1):
    pass        

class GGP2(object):
    def on_start(self):
        print('GGP2 do something')

class GP2(GGP2):
    pass

class Parent2(GP2):
    pass            

class Child(Parent1, Parent2):
    def on_start(self):
        for base in Child.__bases__:
            try:
                base.on_start(self)
            except AttributeError:
                # handle that one of those does not have that method
                print('"{}" does not have an "on_start"'.format(base.__name__))

>>> Child().on_start()
GP1 do something else
GGP2 do something
# Note that 'GGP1 do something' is NOT printed since on_start was satisfied by 
# a descendant class L to R, bottom to top

Now imagine a more complex inheritance structure:

enter image description here

If you want each and every forbearer's on_start method, you could use __mro__ and filter out the classes that do not have on_start as part of their __dict__ for that class. Otherwise, you will potentially get a forbearer's on_start method. In other words, hassattr(c, 'on_start') is True for every class that Child is a descendant from (except object in this case) since Ghengis has an on_start attribute and all classes are descendant classes from Ghengis.

** Warning -- Demo Only **

class Ghengis(object):
    def on_start(self):
        print('Khan -- father to all')    

class GGP1(Ghengis):
    def on_start(self):
        print('GGP1 do something')

class GP1(GGP1):
    pass

class Parent1(GP1):
    pass        

class GGP2(Ghengis):
    pass

class GP2(GGP2):
    pass

class Parent2(GP2):
    def on_start(self):
        print('Parent2 do something')

class Child(Parent1, Parent2):
    def on_start(self):
        for c in Child.__mro__[1:]:
            if 'on_start' in c.__dict__.keys():
                c.on_start(self)

>>> Child().on_start()
GGP1 do something
Parent2 do something
Khan -- father to all

But this also has a problem -- if Child is further subclassed, then the child of Child will also loop over the same __mro__ chain.

As stated by Raymond Hettinger:

super() is in the business of delegating method calls to some class in the instance’s ancestor tree. For reorderable method calls to work, the classes need to be designed cooperatively. This presents three easily solved practical issues:

1) the method being called by super() needs to exist

2) the caller and callee need to have a matching argument signature and

3) every occurrence of the method needs to use super()

The solution is to write cooperative classes that uniformly use super through the ancestor list or creative use of the adapter pattern to adapt classes you cannot control. These methods are discussed more completely in the article Python’s super() considered super! by Raymond Hettinger.

like image 157
dawg Avatar answered Sep 21 '22 12:09

dawg