Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The real solution for multiple inheritance with different init parameters

This seems like such a simple task, but I've spend way too much time on this now, without a solution. Here's the setup:

class A(object):
  def __init__(self, x=0):
    print("A.__init__(x=%d)" % x)

class B(object):
  def __init__(self, y=1):
    print("B.__init__(y=%d)" % y)

class C(A, B):
  def __init__(self, x=2, y=3, z=4):
    super().__init__(x=x, y=y)
    print("C.__init__(z=%d)" % z)

That's the idea, but of course this results in

TypeError: __init__() got an unexpected keyword argument 'y'

All other attempts have failed in a similar manner, and I could find no resource on the internet with the correct solution. The only solutions included replacing all init params with *args, **kwargs. This does not really suit my needs.


As per request, a real world example:
(This uses a different approach, which has a valid syntax, but produces unwanted results.)

from PyQt5.QtCore import QObject

class Settings(object):
    def __init__(self, file):
        self.file = file

class SettingsObject(object):
    def __init__(self, settings=None):
        print("Super Init", settings is None)
        self.settings = settings

class MyObject(QObject, SettingsObject):
    def __init__(self, param):
        print("Entering Init")
        QObject.__init__(self)
        SettingsObject.__init__(self, settings=Settings(__file__))
        self.param = param
        print("Leaving Init")

Result:

Entering Init
Super Init True
Super Init False
Leaving Init

I would like the line Super Init True to disappear.

like image 622
yspreen Avatar asked Jun 10 '17 17:06

yspreen


1 Answers

It seems there's a few things you're confused about, so:

  1. Why do you get TypeError: __init__() got an unexpected keyword argument 'y'?

    Because the next implementation of __init__ in the method resolution order (MRO) is A.__init__, which only accepts x. The MRO is C -> A -> B, so you must make A.__init__ accept y (either specifically or using **kwargs) and pass it on (using super again) to B.__init__.

    super doesn't call every implementation, and certainly doesn't ensure they all get only the arguments they're expecting, it just calls the next implementation with all of the parameters it's given.

  2. Why does SettingsObject.__init__ get called twice, once with and once without settings?

    It would appear that QObject.__init__ includes a call to super().__init__, which invokes SettingsObject.__init__ as that's next in MyObject's MRO. However it doesn't pass along any settings parameter, so the first time it's invoked you see settings is None. The second time you're invoking it directly, and passing settings explicitly, so you see settings is not None.

  3. How do you write mix-in classes?

    I think this is the question you should really be asking. SettingsObject should be a mix-in class, and therefore be designed correctly to collaborate with other classes in the hierarchy it's mixed into. In this case:

    class SettingsObject:  # (object) isn't needed in 3.x
        def __init__(self, *args, settings=None, **kwargs):
            super().__init__(*args, **kwargs)
            self.settings = settings
    

    It looks from your example like QObject.__init__ doesn't have any required parameters, but you should still write the mix-in to play nice in case of optional parameters or reuse elsewhere. Then the MyObject implementation looks like:

    class MyObject(SettingsObject, QObject):
    
        def __init__(self, param):
            super().__init__(settings=Settings(file))
            self.param = param
    

    Note that:

    • mix-in classes appear first when subclassing, so the MRO is MyObject -> SettingsObject -> QObject; and
    • you call super().__init__, once, not each of the superclass implementations separately.

As an alternative, have you considered composition? Perhaps MyObject should take the settings:

 class MyObject(QObject):

     def __init__(self, param, settings=None):
         super().__init__()
         self.param = param
         self.settings = settings

obj = MyObject('something', Settings(__file__))

Now you call self.settings.method(...) instead of self.method(...), but you don't have the complexity that multiple inheritance introduces.

like image 118
jonrsharpe Avatar answered Oct 29 '22 18:10

jonrsharpe