6

I am writing a Python framework. In order to ensure a class has some properties, I make base "interface" classes like:

class BananaContainer:
    def __init__(self):
        self._bananas = []
    @property
    def bananas(self):
        return self._bananas

Then, if an object is supposed to be a container of bananas, I just derive it from BananaContainer.

A potential problem arise with objects being multiple containers. In my team we are asking ourselves whether multiple inheritance is the appropriate way to go or if some alternative solutions exist.

  • is it correct to have an object inheriting both from BananaContainer and AppleContainer for example ?
    • can it hit us back later with problems like method resolution order or name collisions ? (we can be careful not having similar properties like .name for example - we would like to limit those interfaces to the minimum required, nothing more)

An alternative implementation is proposed via "capabilities" based on composition instead of multiple inheritance. See below for an example of what is proposed:

class BaseCapabilities:
    EXISTING_CAPABILITIES = {"banana": BananaContainer, "apple": AppleContainer, ...}

    def get_capability(self, name):
        if name in self.capabilities():
            return EXISTING_CAPABILITIES[name](self)
    def capabilities(self):
        # should return a list of capabilities
        raise NotImplementedError

Each object would have to derive from 1 class only, but then it needs to implement the capabilities machinery:

 class MyContainer(BaseCapabilities):
     def capabilities(self):
         return ["banana", "apple"]

Then, within the framework it is possible to check if an object has a desired capability and to get an instance of the container class... This is to avoid multiple inheritance considered harmful.

My preferred choice would be to go for multiple inheritance and to use isinstance(obj, BananaContainer) for example to know if obj is a banana container. But I understand my colleague arguments too.

I would be very grateful to get some help to make a decision on this question.

2

3 Answers 3

5

Using Python's multiple inheritance (MI) for interfaces, abstract base classes, mixins, or similar techniques is perfectly fine. In most cases, the MRO produces intuitive results.

However, object initialization under multiple inheritance is really tricky. In Python you cannot combine multiple classes per MI unless all participating classes have been designed for MI. The issue is that the __init__() method cannot know from which class it will be called and what the signature of the super().__init__() method will be. Effectively, this means that MI constructors:

  • must call the super().__init__()
  • must only take arguments by name, not by position
  • must forward any **kwargs to the super().__init__()
  • must not warn on unexpected arguments

Where possible, the better alternative is to avoid __init__() methods for interface-like classes, and instead express requirements through abstract methods. For example, instead of a BananaContainer class, we might write this interface/ABC:

import abc  # abstract base class

class BananaContainer(abc.ABC):
  @property
  @abc.abstractmethod
  def bananas(self) -> list:
    raise NotImplementedError

If a class wants to be a BananaContainer, it would have to implement that property.

In general, it is perfectly alright if you have a class that inherits from multiple interfaces or mixins. Aside from name collisions, the above __init__() problems, and general API bloat of the class, no noteworthy issues arise.


The second part of your question proposes a capability-based approach instead of using inheritance. Using composition instead of inheritance is often a very very good idea. For example, you eliminate the initialization problems by design. It also tends to lead to more explicit APIs that are easier to navigate and avoid name clashes. There should be some method that either returns an object representing a capability, or None if the capability isn't supported.

But these capabilities can be implemented in different ways: either by using normal composition, or by storing the capabilities in your own data structures.

  • Unless you have special needs for the object model, stick to the language. Store methods in normal object fields, provide normal methods to access them. This leads to a more comfortable API, and is more likely to support auto-completer and type-checkers.

  • If you need to modify the available capabilities of an object at run-time, and need to introduce new kinds of capabilities at run-time, then using a dictionary may be appropriate. But at this point you are inventing your own object system. This may be a good idea e.g. in games that have complex capability systems where new capabilities shall be defined in configuration files.

    Most software does not have these requirements, and does not benefit from that kind of flexibility.

    Additionally, Python's built-in object system is flexible enough that you could create new types and new methods without having to create a new object system. Builtins like getattr(), setattr(), hasattr(), and the type() constructor come in handy here.

I would likely express an object that can have both AppleContainer and BananaContainer capabilities like this:

class BananaContainer:
  ...

class AppleContainer:
  ...

class HasCapabilities:
  def __init__(self, x, y, z):
    # somehow determine the appropriate capabilities and initialize them
    self._banana_container = BananaContainer(y) if x else None
    self._apple_container = AppleContainer(y)

  @property
  def as_banana_container(self) -> Optional[BananaContainer]:
    return self._banana_container

  @property
  def as_apple_container(self) -> Optional[AppleContainer]:
    return self._apple_container

o = HasCapabilities(...)
bc = o.as_banana_container
if bc is not None:
  bc.do_banana_things()

Or with Python 3.8 assignment expressions:

if (bc := o.as_banana_container) is not None:
  bc.do_banana_things()

If you want to have some custom mechanisms for reflection over capabilities, you can implement that on top of this solution, with some amount of boilerplate. If we want to be MI-safe, we might declare the following base class that all capability-having classes need to inherit:

class CapabilityReflection:
  # a base implementations so that actual implementations
  # can safely call super()._get_capabilities()
  def _list_capabilities(self):
    return ()

  def all_capabilities(self):
    """deduplicated set of capabilities that this object supports."""
    set(self._list_capabilities())

  def get_capability(self, captype):
    """find a capability by its type. Returns None if not supported."""
    return None

which in the above case would have been implemented as:

class HasCapabilities(CapabilityReflection):
  ...
  def _list_capabilities(self):
    caps = [  # go through properties in case they have been overridden
      self.as_banana_container,
      self.as_apple_container,
    ]
    yield from (cap for cap in caps if cap is not None)
    yield from super()._list_capabilities()

  def get_capability(self, captype):
    if captype == BananaContainer:
      return self.as_banana_container
    if captype == AppleContainer:
      return self.as_apple_container
    return super().get_capability(captype)
3
  • Thanks for reminding me about the abc module. About the second part of your answer: I like its simplicity, compared to having a formal capabilities class. But then, when using the object it ends up doing duck typing, right? You cannot ask for a specific capability using a common API call. I am writing a framework, and thus I am not very comfortable with forcing framework users to do duck typing on the framework objects
    – mguijarr
    Commented Sep 21, 2019 at 9:15
  • @mguijarr It certainly makes sense to think about what API would be most convenient for the users of the API – Python makes duck typing easy, and you can assert certain shapes up front with calls like assert hasattr(foo, 'some_property'). But sometimes explicit methods are better. I've added an example for querying available capabilities in an MI-safe manner. Which approach is appropriate will depend on the exact purpose of your framework. I tend towards low-magic, more-explicit approaches.
    – amon
    Commented Sep 21, 2019 at 9:54
  • +1 for the reference of new syntax for python 3.8 (always good to learn new things). In this case, inheritance is used to implement a mixin approach. In order to ensure capabilities - in the inheritance scenario you can use type checking - like isinstanceof which makes it a bit clearer to define intention. Composition is probably more appropriate to express a scenario for multiple unrelated capabilities, but I would go with a pulgin approach, providing a cleaner way to expand (which will require less boilerplate code than the example provided). In any case, love the answer. Commented Jun 20, 2020 at 14:25
2

In order to ensure a class has some properties, I make base "interface" classes

While this is a common design pattern in statically typed languages, Python programmers consider more idiomatic to use duck typing for your classes. Since the language is dynamically typed, if you have Foo and Bar classes that both can contain bananas, you are free to call unknown.banana on a variable that can be either. If unknown can be an object that don't implement banana, you can also use getattr or try/except AttributeError blocks. The explicit interface is just bloat over features the language already support.

If for any reason you don't want to get rid of these interfaces, then you could at least use multiple inheritance. It exists because it has uses and is correct to use in many cases.

can it hit us back later with problems like method resolution order or name collisions ?

In your multiple inheritance declaration, the first object has priority when it comes to symbol collisions. In some cases, it's a feature, but you have to be careful this doesn't cause unintended overrides, like you would when defining methods and properties in a child class.

The capability suggestion is overly defensive over inheritance mechanisms. Making sure you don't accidentally override is your responsibility, but it shouldn't be a huge burden. If it happens to be one, it's likely you have other problems. And in cases you are not sure of the symbols contained in a class and want to use it as a black box, it may be appropriate to favor composition.

3
  • 1
    I know about duck typing, and can appreciate it sometimes but as I am designing a framework, users of the framework want to feel "safe" and "guided" and appreciate proper interfaces. Thanks for your answer.
    – mguijarr
    Commented Sep 21, 2019 at 9:04
  • @mguijarr I think I missed the most important part where you said you are designing a framework. I'll rewrite parts of this.
    – Diane M
    Commented Sep 21, 2019 at 9:06
  • Well nevermind, I don't have time and I believe I can't top amon's answer :)
    – Diane M
    Commented Sep 21, 2019 at 9:12
-2

A sub class will use the attributes of the parent class. You can add more attributes without disturbing the parent class which is useful. Using the super().__init__ method will let you set the attributes from the parent class into the subclass. In example:

class BananaContainer:
    def __init__(self, _bananas):
        self._bananas = _bananas


user1 = BananaContainer(0)
print(user1._bananas)


class AddBananas(BananaContainer):
    def __init__(self):
        super().__init__(_bananas=20)


user2 = AddBananas()
print(user2._bananas)

The output:

0
20

This can be used in a number of ways usually with an input method that will append the variable. To inherit from multiple classes I would use a function that reads them separately initially, then iconfies the result.

2
  • 1
    Sorry @Barb this does not answer my questions at all
    – mguijarr
    Commented Sep 22, 2019 at 16:25
  • I dont think you will learn from someone else writing your code for you. The answer to your question is yes you can inherit from multiple classes and no it wont be a problem later on if you code it correct to begin with. Hence my example of how to use classes above. I hope this answers your question with more clarity
    – Barb
    Commented Sep 22, 2019 at 17:04

Not the answer you're looking for? Browse other questions tagged or ask your own question.