Python Forum
Multiple inheritance - the right way ?
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Multiple inheritance - the right way ?
#1
Hello,

I am not sure to fully understand multiple inheritance with Python 3. I can implement what I want to implement. It works. However, I am not sure to do things as they should be done.

My questions have to do with the constructor (namely "__new__") and the initializer (namely "__init__").

Below, I implement a (very simple) use case :

[Image: inheritance-multiple.png]

The simplified code (that shows the implementation skeleton) is :

class Parent1:

    def __new__(cls, *args, **kwargs):
        return super(__class__, cls).__new__(cls)

    def __init__(self, *args, **kwargs):
        pass

class Parent2:

    def __new__(cls, *args, **kwargs):
        return super(__class__, cls).__new__(cls)

    def __init__(self, *args, **kwargs):
        pass

class Child(Parent1, Parent2):

    def __new__(cls, *args, **kwargs):
        return super(__class__, cls).__new__(cls, *args, **kwargs)

    def __init__(self, *args, **kwargs):
        Parent1.__init__(self, *args, **kwargs)
        Parent2.__init__(self, *args, **kwargs)

c = Child(1, 2, 3, name='C')
And I also give a slightly "richer" implementation, which contains code that dumps some interesting information.

Please note that the code below is, in essence, identical to the above one. The only difference between the two is that the latter produces outputs.

from pprint import pformat

class Parent1:

    def __new__(cls, *args, **kwargs):
        print("%s: Info:\n\t* __class__ = %s\n\t* cls = %s" %(__class__.__name__,
                                                              __class__.__name__,
                                                              pformat(cls)))
        print("%s: new:\n\t* %s%s%s" %(__class__.__name__,
                                       cls.__name__,
                                       (",\n\t* " if len(args) > 0 else '') + ', '.join(map(lambda x: '"%s"'%str(x), list(args))),
                                       (",\n\t* " if len(kwargs) > 0 else '') + ', '.join(map(lambda x: '%s="%s"'%(str(x[0]), str(x[1])), kwargs.items()))))
        o = super(__class__, cls).__new__(cls)
        # super(type, type2) -> bound super object; requires issubclass(type2, type)
        print("%s: cls(that is: %s) is subclass of %s ? %s" %(__class__.__name__,
                                                              cls.__name__,
                                                              __class__.__name__,
                                                              "true" if issubclass(cls, __class__) else "false"))
        print('%s: o = %s' %(__class__.__name__, pformat(o)))
        return o

    def __init__(self, *args, **kwargs):
        print('%s: self = %s' %(__class__.__name__, self))
        print('%s: init(%s%s)' %(__class__.__name__,
                                 ', '.join(map(lambda x: '"%s"'%str(x), list(args))),
                                 (', ' if len(kwargs) > 0 else '') + ', '.join(map(lambda x: '%s="%s"'%(str(x[0]), str(x[1])), kwargs.items()))))


class Parent2:

    def __new__(cls, *args, **kwargs):
        print("%s: Info:\n\t* __class__ = %s\n\t* cls = %s" %(__class__.__name__,
                                                              __class__.__name__,
                                                              pformat(cls)))
        print("%s: new:\n\t* %s%s%s" %(__class__.__name__,
                                      cls.__name__,
                                      (",\n\t* " if len(args) > 0 else '') + ', '.join(map(lambda x: '"%s"'%str(x), list(args))),
                                      (",\n\t* " if len(kwargs) > 0 else '') + ', '.join(map(lambda x: '%s="%s"'%(str(x[0]), str(x[1])), kwargs.items()))))
        o = super(__class__, cls).__new__(cls)
        # super(type, type2) -> bound super object; requires issubclass(type2, type)
        print("%s: cls(that is: %s) is subclass of %s ? %s" %(__class__.__name__,
                                                              cls.__name__,
                                                              __class__.__name__,
                                                              "true" if issubclass(cls, __class__) else "false"))
        print('%s: o = %s' %(__class__.__name__, pformat(o)))
        return o

    def __init__(self, *args, **kwargs):
        print('%s: self = %s' % (__class__.__name__, self))
        print('%s: init(%s%s)' %(__class__.__name__,
                                 ', '.join(map(lambda x: '"%s"'%str(x), list(args))),
                                 (', ' if len(kwargs) > 0 else '') + ', '.join(map(lambda x: '%s="%s"'%(str(x[0]), str(x[1])), kwargs.items()))))

class Child(Parent1, Parent2):

    def __new__(cls, *args, **kwargs):
        print("%s: Info:\n\t* __class__ = %s\n\t* cls = %s" %(__class__.__name__,
                                                              __class__.__name__,
                                                              pformat(cls)))
        print("%s: new:\n\t* %s%s%s" %(__class__.__name__,
                                       cls.__name__,
                                       (",\n\t* " if len(args) > 0 else '') + ', '.join(map(lambda x: '"%s"'%str(x), list(args))),
                                       (",\n\t* " if len(kwargs) > 0 else '') + ', '.join(map(lambda x: '%s="%s"'%(str(x[0]), str(x[1])), kwargs.items()))))
        o = super(__class__, cls).__new__(cls, *args, **kwargs)

        # o = Top1.__new__(cls, *args, **kwargs)

        # super(type, type2) -> bound super object; requires issubclass(type2, type)
        print("%s: cls(that is: %s) is subclass of %s ? %s" %(__class__.__name__,
                                                              cls.__name__,
                                                              __class__.__name__,
                                                              "true" if issubclass(cls, __class__) else "false"))
        print('%s: o = %s' %(__class__.__name__, pformat(o)))
        return o

    def __init__(self, *args, **kwargs):
        print('%s: self = %s' %(__class__.__name__, self))
        print('%s: init(%s%s)' %(__class__.__name__,
                                 ', '.join(map(lambda x: f'"%s"'%str(x), list(args))),
                                 (', ' if len(kwargs) > 0 else '') + ', '.join(map(lambda x: '%s="%s"'%(str(x[0]), str(x[1])), kwargs.items()))))

        # This is OK:
        Parent1.__init__(self, *args, **kwargs)
        Parent2.__init__(self, *args, **kwargs)


c = Child(1, 2, 3, name='C')
The output is:

Output:
$ python3 inheritance-multiple.py Child: Info: * __class__ = Child * cls = <class '__main__.Child'> Child: new: * Child, * "1", "2", "3", * name="C" Parent1: Info: * __class__ = Parent1 * cls = <class '__main__.Child'> Parent1: new: * Child, * "1", "2", "3", * name="C" Parent2: Info: * __class__ = Parent2 * cls = <class '__main__.Child'> Parent2: new: * Child Parent2: cls(that is: Child) is subclass of Parent2 ? true Parent2: o = <__main__.Child object at 0x7f5fc94b4550> Parent1: cls(that is: Child) is subclass of Parent1 ? true Parent1: o = <__main__.Child object at 0x7f5fc94b4550> Child: cls(that is: Child) is subclass of Child ? true Child: o = <__main__.Child object at 0x7f5fc94b4550> Child: self = <__main__.Child object at 0x7f5fc94b4550> Child: init("1", "2", "3", name="C") Parent1: self = <__main__.Child object at 0x7f5fc94b4550> Parent1: init("1", "2", "3", name="C") Parent2: self = <__main__.Child object at 0x7f5fc94b4550> Parent2: init("1", "2", "3", name="C")
Focusing on the constructors (__new__), you can see that:
  • Child.__new__ starts its execution.
  • Parent1.__new__ starts its execution.
  • Parent2.__new__ executes completely.
  • Parent1.__new__ terminates its execution.
  • Child.__new__ terminates its execution.

That is:

[Image: inheritance-multiple-sequence.png]


Question 1

I have tried to pass parameters to both constructors (Parent1.__new__ and Parent2.__new__). However, I can only pass parameters to one constructor. The constructor which receives the parameters is the one for the parent class that appears first in the declaration of the child class. That is:

If:

class Child(Parent1, Parent2):
        pass
Then, the contructor that receives the parameters is Parent1.__new__.

If:

class Child(Parent2, Parent1):
        pass
Then, the contructor that receives the parameters is Parent2.__new__.

I am OK with that. I understand that this is due to the "Method Resolution Order".

However:
  • why does Parent1.__new__ triggers the execution of __Parent2.new__ ? This seems weird.
  • how do I do if I want both constructor to receive parameters ?

Please note that I don't see any reason to pass parameters to the constructors since the object initialisation is done elsewhere (in the method __init__). However, if you can pass parameters to one constructor, then why shouldn't you be able to pass parameters to both constructors ?

Question 2

In order to initialise the parent classes, I use the code below:

        Parent1.__init__(self, *args, **kwargs)
        Parent2.__init__(self, *args, **kwargs)
This works. But I wondered: what if I want to use "super" instead ?

I tried:

        super(Parent1, self).__init__(*args, **kwargs)
        super(Parent2, self).__init__(*args, **kwargs)
But this strategy does not work. It throws an error.

Error:
Traceback (most recent call last): File "inheritance-multiple.py", line 94, in <module> c = Child(1, 2, 3, name='C') File "inheritance-multiple.py", line 89, in __init__ super(Parent2, self).__init__(*args, **kwargs) TypeError: object.__init__() takes no parameters
This seems weird since, the documentation for the function "super" says:

Quote:super() -> same as super(__class__, <first argument>)
super(type) -> unbound super object
super(type, obj) -> bound super object; requires isinstance(obj, type)
super(type, type2) -> bound super object; requires issubclass(type2, type)

I think that when I write:

        super(Parent1, self).__init__(*args, **kwargs)
The third use case applies:

Quote:super(type, obj) -> bound super object; requires isinstance(obj, type)

Since "self" is an instance of "Parent1". Then, the returned object should be an instance of "Parent1" bound to the current object "self".

Could you explain why the use of "super" does not work ?

Thanks a lot !

Denis
Reply
#2
Why are you messing with __new__? I would leave that alone. In 15 years of Python programming I have never once needed to mess with __new__. You should be able to do everything with __init__.

You should just need one call to super in each __init__. But you don't go all the way up to object. If you are trying to do a diamond style MRO, you should start with your own base class at the top of the diamond, so you can control the parameters. Then you can have super in all three of the derived classes, and super will follow the MRO through the supers.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#3
Note:

I find a better way to initialise the parent objects:

        # This is OK:
        # Parent1.__init__(self, *args, **kwargs)
        # Parent2.__init__(self, *args, **kwargs)

        for parent in __class__.__bases__:
            parent.__init__(self, *args, **kwargs)
This strategy is better because the parents classes are not hard-coded.

However, I can't find a way to use the function "super()" (to perform the same actions).

(Feb-13-2019, 04:34 PM)ichabod801 Wrote: Why are you messing with __new__? I would leave that alone. In 15 years of Python programming I have never once needed to mess with __new__. You should be able to do everything with __init__.

You should just need one call to super in each __init__. But you don't go all the way up to object. If you are trying to do a diamond style MRO, you should start with your own base class at the top of the diamond, so you can control the parameters. Then you can have super in all three of the derived classes, and super will follow the MRO through the supers.

Yes, I agree with you. As I said, I don't see any good reason to pass parameters to the constructors. Since it's what initializers are there for.

However, I was exploring multiple inheritance and I wondered : if we can do it, there must be a use for it. Or maybe this is a "legacy thing".
Reply
#4
This is how you run through the MRO:

class Top(object):

    def __init__(self):
        print('In class Top.')

class Left(Top):

    def __init__(self):
        print('In class Left.')
        super(Left, self).__init__()

class Right(Top):

    def __init__(self):
        print('In class Right.')
        super(Right, self).__init__()

class Bottom(Left, Right):

    def __init__(self):
        print('In class Bottom.')
        super(Bottom, self).__init__()

b = Bottom()
Output:
In class Bottom. In class Left. In class Right. In class Top.
You can't do that with __class__.__bases__, because Bottom will call Left and Right, and both of them will call Top, so Top will be called twice.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#5
Also with super() in Python 3 can avoid referring to the base class explicitly.
super(Left, self).__init__()
# Look better,that this don't work in Python 2 should we not care about anymore
super().__init__()
Reply
#6
Old habit die hard.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#7
@ichabod801

Yes, good point. If two classes have a parent in common, you cannot use the list "__base__".

I was thinking that I could include within the parent class a flag that indicates whether class has been initialised or not : thus the parent class will be initialised only once. However, I think that this would be a bad practice. Indeed, this technique is tightly bound to a specific Python implementation. If Python changes the way it treats multiple inheritance in the future, then this technique will produce errors.

(Feb-13-2019, 05:51 PM)ichabod801 Wrote: snippsat

Yes. I've seen.

However, the function "super" have multiple signatures.

Quote: super() -> same as super(__class__, <first argument>)
super(type) -> unbound super object
super(type, obj) -> bound super object; requires isinstance(obj, type)
super(type, type2) -> bound super object; requires issubclass(type2, type)

It seems that we can call "super()" on the class other than the current one (that is "__class__").

For example, we can call super on a parent class, from a child class. But I cannot see any good reason to do that.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Multiple Inheritance - Help pls! magnusbrigido 1 1,832 May-17-2019, 12:56 PM
Last Post: ichabod801
  Multiple Inheritance using super() Sagar 2 7,278 Sep-08-2017, 08:58 AM
Last Post: Sagar

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020