2

I am trying to design a class structure that allows the user to define their own class that overloads predefined methods in other classes. In this case the user would create the C class to overload the "function" method in D. The user created C class has common logic for other user created classes A and B so they inherit from C to overload "function" but also inherit from D to use D's other methods. The issue I am having is how to pass "value" from A and B to D and ignore passing it to C. What I currently have written will produce an error as C does not have "value" as an argument.

I know that I can add "value" (or *args) to C's init method and the super call but I don't want to have to know what inputs other classes need in order to add new classes to A and B. Also, if I swap the order of C and D I won't get an error but then I don't use C's overloaded "function". Is there an obvious way around this?

class D(SomethingElse):
    def __init__(self, value, **kwargs):
        super(D, self).__init__(**kwargs)

        self.value = value

    def function(self):
        return self.value

    def other_method(self):
        pass

class C(object):
    def __init__(self):
        super(C, self).__init__()

    def function(self):
        return self.value*2

class B(C, D):
    def __init__(self, value, **kwargs):
        super(B, self).__init__(value, **kwargs)

class A(C, D):
    def __init__(self, value, **kwargs):
        super(A, self).__init__(value, **kwargs)



a = A(3)
print(a.function())
>>> 6
11
  • What should the output be? Commented Jan 31, 2020 at 1:15
  • I should be 6. I edited the post to reflect this. Commented Jan 31, 2020 at 1:16
  • It seems python2? It got different error. I got an error in class A on line super(A, self).__init__(value, **kwargs), __init__() takes 1 positional argument but 2 were given because super(A, self) tries to refer class C and its __init__ doesn't recieve any positional arguments. I had to add *args, **kwargs to mehtioned __init__ and write super(C, self).__init__(*args, **kwargs) and everything worked. It was python3.6. Commented Jan 31, 2020 at 1:19
  • I'm aware it doesn't work. That is what I posted this. I am also aware adding *ags fixes this. I'm just wondering if there is away around adding these. C is self contained and is only overloading methods it doesn't need to know what the input arguments are. I'm wondering if there is a way to code this without needing args and kwargs. Commented Jan 31, 2020 at 1:23
  • There's method resolution order (MRO). When function is missing, python tries to search it somewhere else (in base class, its base class and so on). When you write super(YourClass, self) you refer to the next class in MRO. You can write print(A.__mro__) youll get [A, C, D, SomethingElse, object]. So that, class C should be aware of arguments class D should recieve. The most convenient way is using *args, **kwargs. Read, please, this realpython.com/lessons/multiple-inheritance-python, may contain more details. Commented Jan 31, 2020 at 1:30

2 Answers 2

1

Essentially, there are two things you need to do to make your __init__ methods play nice with multiple inheritance in Python:

  1. Always take a **kwargs parameter, and always call super().__init__(**kwargs), even if you think you are the base class. Just because your superclass is object doesn't mean you are last (before object) in the method resolution order.
  2. Don't pass your parent class's __init__ arguments explicitly; only pass them via **kwargs. Your parent class isn't necessarily the next one after you in the method resolution order, so positional arguments might be passed to the wrong other __init__ method.

This is called "co-operative subclassing". Let's try with your example code:

class D:
    def __init__(self, value, **kwargs):
        self.value = value
        super().__init__(**kwargs)
    
    def function(self):
        return self.value

class C:
    # add **kwargs parameter
    def __init__(self, **kwargs):
        # pass kwargs to super().__init__
        super().__init__(**kwargs)
    
    def function(self):
        return self.value * 2

class B(C, D):
    # don't take parent class's value arg explicitly
    def __init__(self, **kwargs):
        # pass value arg via kwargs
        super().__init__(**kwargs)

class A(C, D):
    # don't take parent class's value arg explicitly
    def __init__(self, **kwargs):
        # pass value arg via kwargs
        super().__init__(**kwargs)

Demo:

>>> a = A(value=3)
>>> a.value
3
>>> a.function()
6

Note that value must be passed to the A constructor as a keyword argument, not as a positional argument. It's also recommended to set self.value = value before calling super().__init__.

I've also simplified class C(object): to class C:, and super(C, self) to just super() since these are equivalent in Python 3.

Sign up to request clarification or add additional context in comments.

5 Comments

This doesn't answer the question, which is how to totally avoid changing the signature of C.__init__ whilst using still super.
@ekhumoro I don't see any part of the question which says or implies that the signature of C.__init__ can't be changed. I'm not sure how you got that from the question; please explain.
1st sentence of the 2nd paragraph (and the comments, where *args, **kwargs etc has already been discussed).
"I don't want to have to know what inputs other classes need in order to add new classes to A and B" Is this the part you mean? It doesn't say the signature can't be changed, only that C.__init__ should not be coupled to the signatures of the other __init__ methods; and using kwargs means it's not. C doesn't know or care what's in kwargs.
If you read the comments as well, and you'll see that the OP wants to avoid having **kwargs as a requirement: "I'm wondering if there is a way to code this without needing args and kwargs".
0

So I'm trying to understand the point of A AND B. I'm guessing that maybe you want to mix in the superclass behavior and sometimes have local behavior. So suppose A is just mixing together behaviors, and B has some local behavior and state.

If you don't need your own state, you probably don't need an __init__. So for A and C just omit __init__.

class SomethingElse(object):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

class D(SomethingElse):
    def __init__(self, value, *args,  **kwargs):
        super(D, self).__init__(*args, **kwargs)

        self.value = value

    def function(self):
        return self.value

    def other_method(self):
        return self.__dict__

class C(object):
    #def __init__(self):
    #    super(C, self).__init__()

    def function(self):
        return self.value*2

class B(C, D):
    def __init__(self, value, bstate, *args, **kwargs):
        super(B, self).__init__(value, *args, **kwargs)
        self.bstate = bstate

    def __repr__(self):
        return (self.__class__.__name__ + ' ' +
                self.bstate + ' ' + str(self.other_method()))

class A(C, D):
    pass


a = A(3)
b = B(21, 'extra')

a.function()
6

b.function()
42
repr(a)
'<xx.A object at 0x107cf5e10>'
repr(b)
"B extra {'args': (), 'bstate': 'extra', 'value': 21, 'kwargs': {}}"

I've kept python2 syntax assuming you might still be using it, but as another answer points out, python3 simplifies super() syntax, and you really should be using python3 now.

If you swap C and D you are changing the python method resolution order, and that will indeed change the method to which a call to A.function resolves.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.