0

I have two classes A and B and both have a method get_type() that returns a string and which is called in the __init__() of both classes to set an instance attribute self.type. Now there's a child class that inherits from both A and B and in it's __init__() it takes an instance of either A or B as an argument. Depending on whether it's instantiated with an instance of A or B, I call the corresponding __init__() of the parent. Presumably because of the lookup order of multiple inheritance, always the get_type() method of the A parent is called, even if C is instantiated with an instance of B. See code below:

class A:
    def __init__(self):
        self.type = self.get_type()
    def get_type(self):
        return 'A'
class B: 
    def __init__(self):
        self.type = self.get_type()
    def get_type(self):
        return 'B'
class C(A, B):
    def __init__(self, instance):
        if isinstance(instance, A):
            A.__init__(self)
            print('Inheriting from A')
        else:
            B.__init__(self)
            print('Inheriting from B')
            
a = A()
b = B()
c = C(b)
print(a.type, b.type, c.type)

>>> Inheriting from B
>>> A B A

So I want that c.type is 'B' since it inherited from the B class. Can I maybe use the super() function to get the behaviour I want?

Pickniclas
  • 349
  • 1
  • 8
  • 1
    This doesn't really make sense. When you use multiple inheritance, you need to call all the superclasses' `__init__()` methods, because other inherited methods may depend on those initializations. – Barmar Mar 03 '23 at 23:58
  • Hey @Barmar, here's it's more like an either or situation. In my real code A and B both inherit from an abstract class and each has its own implementation for a bunch of methods. – Pickniclas Mar 04 '23 at 00:03
  • 1
    This isn't how object oriented design works and based on your comment this sounds like an X/Y problem. – Woodford Mar 04 '23 at 00:12
  • @woodford not sure I get what you mean. So the reasoning for that structure is something like this: There are two classes that represent 'two flavours' of a more general model. But it's always flavor one or two, so the general model is implemented as an abstract class and both share a bunch of methods, but have their own as well. Now I C is basically an extension of either flavor one or two. It's again one of the two flavors but builds on that with some additional input. And again, it makes use of a bunch of methods that are defined in the abstract, general class (which is not in this example) – Pickniclas Mar 04 '23 at 00:30
  • @woodford But i'm always open to suggestions for better software design. Still, I wonder whether I can get the desired behaviour with a structure like I gave above. – Pickniclas Mar 04 '23 at 00:31
  • 1
    "Now there's a child class that inherits from both A and B and in it's __init__() it takes an instance of either A or B as an argument. Depending on whether it's instantiated with an instance of A or B, I call the corresponding __init__() of the paren" This doesn't make any sense. You shouldn't be using multiple inheritance here. Use delegation and composition – juanpa.arrivillaga Mar 04 '23 at 00:50
  • 1
    "Can I maybe use the super() function to get the behaviour I want?" no, that is for cooperative multiple inheritance, where every class in the inheritance hierarchy calls `super` and *all* the methods in the method resolution order end up getting called. What you are describing here is not what you would use `super` for. You *could* use `super` to get there, but it's essentially going to look like what you already have, and you are basically going against what super does, i.e., gives you the *next* method in the method resolution order (you want to *ignore* that order) – juanpa.arrivillaga Mar 04 '23 at 00:51
  • hey @juanpa.arrivillaga thanks for the answer. Ok, so I heard of composition before. But I guess that is usually used to avoid having too many different subclasses, no? The thing is that the child class behaves again like either class A or B, but it has some additional shared functionality. What would delegation mean in this context? – Pickniclas Mar 04 '23 at 00:54
  • @Pickniclas no, composition is just the most basic thing you can do. `self.attribute = 1` is technically composition. Delegation means you *delegate* behavior to one of the attributes you have. Note, you are *always* inheriting *both* from `A` and `B`, you can't choose what to inherit from, inheritance applies to the *type* not a particular instance. The type already inherits from both, because oyu used multiple inheritance – juanpa.arrivillaga Mar 04 '23 at 00:56
  • So, look at [this answer](https://stackoverflow.com/questions/56746709/can-i-choose-the-parent-class-of-a-class-from-a-fixed-set-of-parent-classes-cond/56747123#56747123) to a related question I answered from a whjile back where someone wanted to tried to use multiple inheritance to mean "inherit from A *or* B" (but multiple inheritance always means "inheritr from A **and** B"). I showed a basic example of delegation (which Python makes relatively easy with `__getattr__`) – juanpa.arrivillaga Mar 04 '23 at 00:57
  • OK, thanks for the reference. I mean one easy workaround would be to replace `self.type = self.get_type()` by `self.type = 'A'`. It would then give me the desired behaviour. But I prefer the `get_type()` version, because it is actually an abstarct method. So if I'd ever have another class that C inherits from there must be a 'get_type()' method implemented and I get an error if I forget it. – Pickniclas Mar 04 '23 at 01:01

3 Answers3

0

The why you are doing that apart, here is what you are asking for:

When you have colliding (or potentially colliding) method names, Python's mechanism to avoid that is prefix the method name with two underscores (__). Most articles and 3rdy party docs on Python refer to this as "the way to create a private attribute/method in Python". It is really not that: Python does not make use of private attributes at all, but for convention (which is using a single _ as prefix.

Using two underscores, instead of one, triggers and internal name transformation of the method or attribute, which adds the class name as a prefix to the target method, in a transparent way. This is called "name mangling".

In the case of your code, it will work out of the box with no further changes. If you inspect the running code (print(dir(self)), for example), you will see that your methods will be named A._A__get_type and B._B__get_type, respectively (and both methods will be present in C)

class A:
    def __init__(self):
        self.type = self.__get_type()
    def __get_type(self):
        return 'A'
class B: 
    def __init__(self):
        self.type = self.__get_type()
    def __get_type(self):
        return 'B'

However, see that any method in C would not be able to "see" .get_type: that method is now "private" to each superclass. Both are reachable from C with their transformed names, if used explicitly, tough.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
0

It seems like you are trying to do some kind of conditional inheritance, here is a similar question, conditional inheritance can be considered an anti-pattern because it can make code harder to understand and maintain, anyways you can implement that with some know patterns like Wrapper/Decorator pattern, there is a very good example of that in the comments. Another pattern you can use is the Composition pattern, here is an example:

class A:
    def __init__(self):
        self.type = self.get_type()

    def get_type(self):
        return 'A'

class B:
    def __init__(self):
        self.type = self.get_type()

    def get_type(self):
        return 'B'

class C:
    @classmethod
    def composed_width(cls, Klass, *args, **kwargs):
        class Composed(cls, Klass):
            pass
        return Composed(*args, **kwargs)

    def get_type_wrapped(self):
        return f'WrappedType[{self.get_type()}]'


c_a = C.composed_width(A)
c_b = C.composed_width(B)

print(c_a.type, c_a.get_type_wrapped())
print(c_b.type, c_b.get_type_wrapped())

>>> A WrappedType[A]
>>> B WrappedType[B]

The method get_type_wrapped is just to give an example of a shared method, the idea is just to create a local class that composes the class C and another class passed as a parameter, and returns an instance of it.

Jonathan Quispe
  • 586
  • 3
  • 10
0

I think I understand what your goal is. First, let's look at why your approach is flawed though:

Problem With The Current Approach

As per your class layout, a C is both an A and a B. The reason is that inheritance forms an "is a" relation. From that, the implied expectation is that the relation fulfills the "Liskov Substitution Principle", i.e. that you can provide a C wherever code expects an A or a B. I believe that this not your intention, although close.

Possible Alternative Approach

You mentioned in the comments that both A and B have a common interface, maybe even implemented as an abstract base class (https://docs.python.org/3/library/abc.html). Let's call it I (as in Interface) to make discussion easier. It doesn't matter if it exists as ABC or is just a convention (classes providing similar methods a.k.a. "Duck Typing"), Python is permissive in this area.

With that interface I, we actuall have these three classes that all three implement this interface. If you now write your code to expect an I (instead of concrete A or B), you should be able to pass either of A, B, or C to it (again, Liskov Substitution).

Further, C doesn't have to inherit both A and B, it can contain one of the two instead. Actually, it could simply contain an I, to make it easier to extend to further such types in the future. Any method required by I (you only have get_type() in your sketch) can be implemented by simply forwarding it to the contained I instance.

Notes

  • Why C in the first place? I have my doubts whether this needs to be a class at all. Not everything is a class and not everything should be one! My guess is that large parts are made obsolete by a common abstract base class. The only real advantage I could see is the ability to exchange the underlying A or B and thus switch the behaviour.
  • Reading get_type() is a code smell. I have an idea that you're implementing a type system (Python has it's own) and that you are trying to implement polymorphism or something similar with it. This doesn't have to be a problem but it may need some scrutiny in the future.
  • Check out https://en.wikipedia.org/wiki/Decorator_pattern, which is similar, and maybe a few other related patterns whether they fit your use case.
Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55