2

I wrote classes with diamond inheritance. Were there are two classes at same level the constructors have different length of arguments list and depending on declaration order in list of bases declaration everything works fine or error in thrown immediately

class DFT(Common.BaseAS, Common.Signal):

    def __init__(self, fs, N, history_len=1, strict=False):

        super().__init__(fs, N, history_len, strict, np.complex)


class BaseAS(abc.ABC, AnalysisResultSaver):
   No constructor here

class AnalysisResultSaver(Base):

    def __init__(self, fs=8000, N=250, history_len=1, strict=False, dtype=None):

        super().__init__(fs, N, dtype)

class Signal(Base):

    def __init__(self, fs, N, dtype=None):
        super().__init__(fs, N, dtype)

class Base:

    def __init__(self, fs, N, dtype=None):
       Stuff  

Constructors are called in order: DFT; AnalysisResultSaver; Signal; Base;

In this case everything works fine but my question is 1) how are arguments passed to Signal constructor if there's no direct indication which arguments are right ones, is it just trimmed to first two?

But if I change order of bases in DFT then I get

super().__init__(fs, N, history_len, strict, np.complex)
TypeError: __init__() takes from 3 to 4 positional arguments but 6 were given

I know it changes mro but in first case it works fine

and if I want to call constructors directly by Common.BaseAS.__init__() and Common.Signal.__init__() than Signal constructor is called twice so somehow call to BaseAS calls Signal constructor even though it's not its parent.

Common.BaseAS.__init__(self, fs, N, history_len, strict, np.complex)
Common.Signal.__init__(self, fs, N)

So 2) how can BaseAS call Signal constuctor?

2 Answers 2

3

In your example the super call in AnalysisResultSaver.__init__ is what calls Signal.__init__. This may be counterintuitive as Signal is not a superclass of AnalysisResultSaver, and is an example of the interesting way multiple inheritance and the super function works in Python.

In particular, when you write super() in AnalysisResultSaver this is really a shorthand for super(AnalysisResultSaver, self). So what does this actually do? It looks at the method resolution order for actual instance you passed in (self) and tries to find the first matching method after the class you passed in (AnalysisResultSaver).

If you print self.__class__ in AnalysisResultSaver as you'd expect you'll see that the object itself is an instance of DFT. If you look at the method resolution order on that class self.__class__.__mro__ or just DFT.__mro__, you'll see a list of classes: DFT, BaseAS, AnalysisResultSaver, Signal, Base, object.

Notice how the first one after AnalysisResultSaver is Signal which is how it decides that Signal.__init__ is the specific constructor it should call next.

If you are interested I suggest you read more about Python's multiple inheritance and the super function specifically; there are many resources online which cover this topic more thoroughly.

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

2 Comments

Good answer, I added one as well to include some running code to help demonstrate what is happening here. This kind of class inheritance structure makes me dizzy.
Thanks, it explains how constructors are being called
2

The answer by @KSab is correct, but I will add this as it helps illustrate what is happening (and was suggested in that answer). I modified your code a bit to show exactly what is happening and in what order as these objects are constructed. Here is the code:

import abc
import numpy as np

class Base:
    def __init__(self, fs, N, dtype=None):
        print('='*80)
        print(f"Base fs: {fs}")
        print(f"Base N: {N}")
        print(f"Base dtype: {dtype}")


class Signal(Base):
    def __init__(self, fs, N, dtype=None):
        print('='*80)
        print(f"Signal self: {self}")
        print(f"Signal fs: {fs}")
        print(f"Signal N: {N}")
        print(f"Signal dtype: {dtype}")
        print("Signal(Base) will now call:  super().__init__(fs, N, dtype)")
        super().__init__(fs, N, dtype)


class AnalysisResultSaver(Base):
    def __init__(self, fs=8000, N=250, history_len=1, strict=False, dtype=None):
        print('='*80)
        print(f"ARS self: {self}")
        print(f"ARS fs:{fs} ")
        print(f"ARS N: {N}")
        print(f"ARS history_len: {history_len}")
        print(f"ARS strict: {strict}")
        print(f"ARS dtype: {dtype}")
        print("ARS(Base) will now call:  super().__init__(fs, N, dtype)")
        super().__init__(fs, N, dtype)

class BaseAS(abc.ABC, AnalysisResultSaver):
    pass

class DFT(BaseAS, Signal):
    def __init__(self, fs, N, history_len=1, strict=False):
        print('='*80)
        print(f"DFT self: {self}")
        print(f"DFT fs:{fs} ")
        print(f"DFT N: {N}")
        print(f"DFT history_len: {history_len}")
        print(f"DFT strict: {strict}")
        print("DFT(BaseAS, Signal) will now call: super().__init__(fs, N, history_len, strict, np.complex)")
        super().__init__(fs, N, history_len, strict, np.complex)



my_d = DFT('fs', 32, 10, True)

It will produce this output:

================================================================================
DFT self: <__main__.DFT object at 0x10cabe310>
DFT fs:fs
DFT N: 32
DFT history_len: 10
DFT strict: True
DFT(BaseAS, Signal) will now call: super().__init__(fs, N, history_len, strict, np.complex)
================================================================================
ARS self: <__main__.DFT object at 0x10cabe310>
ARS fs:fs
ARS N: 32
ARS history_len: 10
ARS strict: True
ARS dtype: <class 'complex'>
ARS(Base) will now call:  super().__init__(fs, N, dtype)
================================================================================
Signal self: <__main__.DFT object at 0x10cabe310>
Signal fs: fs
Signal N: 32
Signal dtype: <class 'complex'>
Signal(Base) will now call:  super().__init__(fs, N, dtype)
================================================================================
Base fs: fs
Base N: 32
Base dtype: <class 'complex'>
================================================================================

Also, this is the MRO for each class:

>>> DFT.mro()
[<class '__main__.DFT'>, <class '__main__.BaseAS'>, <class 'abc.ABC'>, <class '__main__.AnalysisResultSaver'>, <class '__main__.Signal'>, <class '__main__.Base'>, <class 'object'>]
>>> BaseAS.mro()
[<class '__main__.BaseAS'>, <class 'abc.ABC'>, <class '__main__.AnalysisResultSaver'>, <class '__main__.Base'>, <class 'object'>]
>>> AnalysisResultSaver.mro()
[<class '__main__.AnalysisResultSaver'>, <class '__main__.Base'>, <class 'object'>]
>>> Signal.mro()
[<class '__main__.Signal'>, <class '__main__.Base'>, <class 'object'>]
>>> Base.mro()
[<class '__main__.Base'>, <class 'object'>]

1 Comment

Thanks a lot. Although I wrote some code with print to it myself, it was your addition of parameters that has let my understand where are constructor call parameters coming from. I see that 'ARS' 'super()' calls 'Signal' constructor and as it's got same parameters set as 'Base' for with the call was intended there's no conflict

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.