spaderdabomb
spaderdabomb

Reputation: 942

Inheriting from classes with and without **kwargs

I am building a plotting class in Python, and am hoping to do the following. I want a graphics window using PyQt5 that also inherits from some custom classes I have made (such as a curve fitting class). In order for the curve fitting class to manipulate data that persists in the plotting class, it must have a reference to the data that is contained in the plotting class. Because of this, I have chosen the plotting class to inherit from the CurveFitting class.

The problem seems to arise in inheriting both from PyQt5's GraphicsWindow class and my custom class, which accept different numbers of arguments. I have read that Python does not play nice with classes that inherit different numbers of arguments using the "super" functionality, so I decided to make my custom CurveFitting class accept **kwargs, which would then give it a reference to the parent. However, I then encountered a different error which I do not understand. Below is a tidied up example of what I'm trying to do

import numpy as np
from pyqtgraph import GraphicsWindow

class ClassA():
    def __init__(self, **kwargs):
        super().__init__()
        self.kwargs = kwargs
        self.parent = self.kwargs['parent']
        self.xdata = self.parent.xdata

    def print_data(self):
        print(self.parent.xdata)
        print(self.parent.ydata)


class classC(GraphicsWindow, ClassA):
    def __init__(self):
        kwargs = {}
        kwargs['parent'] = self
        kargs = kwargs
        self.xdata = np.linspace(0, 100, 101)
        self.ydata = np.linspace(0, 200, 101)

        super().__init__(**kwargs)
        # ClassA.__init__(self, **kwargs)
        # GraphicsWindow.__init__(self)

instC = classC()
instC.print_data()

When I run the above I get "RuntimeError: super-class init() of type classC was never called" on the "super().__init(**kwargs)" line, which I honestly do not understand at all, and have tried googling for a while but to no avail.

Additionally, I have tried commenting out the line, and uncommenting the next two lines to inherit from each class manually, but this also does not work. What I find pretty weird is that if I comment one of those two lines out, they both work individually, but together they do not work. For example, if I run it with both lines, it gives me an error that kwargs has no key word 'parent', as if it didn't even pass **kwargs.

Is there a way to inherit from two classes that take a different number of initialization parameters like this? Is there a totally different way I could be approaching this problem? Thanks.

Upvotes: 1

Views: 797

Answers (1)

Blckknght
Blckknght

Reputation: 104722

The immediate problem with your code is that ClassC inherits from GraphicsWindow as its first base class, and ClassA is the second base class. When you call super, only one gets called (GraphicsWindow) and if it was not designed to work with multiple inheritance (as seems to be the case), it may not call super itself or may not pass on the arguments that ClassA expects.

Just switching the order of the base classes may be enough to make it work. Python guarantees that the base classes will be called in the same relative order that they appear in the class statement in (though other classes may be inserted between them in the MRO if more inheritance happens later). Since ClassA.__init__ does call super, it should work better!

It can be tricky to make __init__ methods work with multiple inheritance though, even if all the classes involved are designed to work with it. This is why positional arguments are often avoided, since their order can become very confusing (since child classes can only add positional arguments ahead of their parent's positional arguments unless they want to repeat all the names). Using keyword arguments is definitely a better approach.

But the code you have is making dealing with keyword arguments a bit more complicated than it should be. You shouldn't need to explicitly create dictionaries to pass on with **kwargs syntax, nor should you need to extract keyword values from a a dict you accepted with a **kwargs argument. Usually each function should name the arguments it takes, and only use **kwargs for unknown arguments (that may be needed by some other class in the MRO). Here's what that looks like:

class Base1:
    def __init__(self, *, arg1, arg2, arg3, **kwargs):  # the * means the other args are kw-only
        super().__init__(**kwargs)                      # always pass on all unknown arguments
        ...                                             # use the named args here (not kwargs)

class Base2:
    def __init__(self, *, arg4, arg5, arg6, **kwargs):
        super().__init__(**kwargs)
        ...

class Derived(Base1, Base2):
    def __init__(self, *, arg2, arg7, **kwargs):         # child can accept args used by parents
        super().__init__(arg2=arg2+1, arg6=3, **kwargs)  # it can then modify or create from
        ...                                              # scratch args to pass to its parents

obj = Derived(arg1=1, arg2=2, arg3=3, arg4=4, arg5=5, arg7=7) # note, we've skipped arg6
                                                              # and Base1 will get 3 for arg2

But I'd also give serious though to whether inheritance makes any sense in your situation. It may make more sense for one of your two base classes to be encapsulated within your child class, rather than being inherited from. That is, you'd inherit from only one of ClassA or GraphicsWindow, and store an instance of the other in each instance of ClassC. (You could even inherit from neither base class, and encapsulate them both.) Encapsulation is often a lot easier to reason about and get right than inheritance.

Upvotes: 2

Related Questions