1

I am trying to better understand composition vs inheritance in python so I have crafted a hopefully simple example.

Suppose I want to implement a class TitledList that is intended to behave just like a standard list, but with an additional attribute title intended to hold a string containing the name of the list. The __repr__ and __str__ methods would likely have to be reimplemented to incorporate the title, but otherwise I'd like to retain all the native functionality of list. Initializers would accept an optional keyword-only argument title.

I can see two approaches to creating this class. I could go the composition route, and define a class with two attributes, .contents (a regular list) and .title. That would presumably look something along the lines of

class TitledList:
    def __init__(self, contents=[], *, title=None):
        self.contents = list(contents)
        self.title = title

But I'm not sure how I would go about cribbing all the methods of list so I don't have to constantly be referring to my_titled_list.contents all over the place or reimplement all the list methods I use.

The other alternative is to do the thing that everyone on the whole Internet says not to do, and inherit from the list class. I think in that case I would do the initializer like this?

class TitledList(list):
    def __init__(self, iterable=[], *, title=None):
        super().__init__(iterable)
        self.title = title

This seems a lot more straightforward to me. But surely everyone on the whole Internet says not to extend list for a reason.

What are the pros and cons here? Is there a simple way to make the composition solution work the way I intuitively want? Are there lots of drawbacks to the inheritance solution that I don't understand? Is there some third option I should be considering (UserList?)

thecommexokid
  • 303
  • 2
  • 13
  • Or maybe my inheritance initializer should be `def __init__(self, *args, title=None, **kwargs):` `super().__init__(*args, **kwargs)`? – thecommexokid Nov 13 '19 at 06:37
  • 1
    You could possibly try inheriting from [MutableSequence](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableSequence), delegating the abstract methods to your wrapped list. This would give you a lot of listy behaviour for free – snakecharmerb Nov 13 '19 at 06:37
  • 1
    This is exactly what `UserList` is for. Use that. – Markus Meskanen Nov 13 '19 at 06:39
  • 2
    What should be the result of `a + b`, if `a` is a `TitledList` and `b` is a `list`? Should this be a `list` or a `TitledList`? Same question for `b + a`. And: What if `b` is also a `TitledList`? What should be the `title` of that result? – Jonathan Scholbach Nov 13 '19 at 06:40
  • @jonathan.scholbach Obviously those would be questions I would have to answer, but how do they favor one approach over the other? I'd need to figure how to resolve that ambiguity either way. – thecommexokid Nov 13 '19 at 06:46
  • @snakecharmerb Not totally sure how to read this page. Is the claim that I only have to explicitly implement `__getitem__`, `__setitem__`, `__delitem__`, `__len__`, and `insert` and everything else comes for free? It's still a very boring 5 implementations but better 5 than (however many methods `list` has). – thecommexokid Nov 13 '19 at 06:49
  • I would say, these questions point to a somewhat conceptual problem of your idea. The nice thing with `list` type is, that there is no ambiguity for these questions. For your `TitledList` you would get problems. I think, this is exactly the reason why `UserList` (if you think about using this) accepts only a constructor with one argument (the list), and no additional parameters. – Jonathan Scholbach Nov 13 '19 at 06:50
  • Yes, that would be the idea; your implementation of, say, `__getitem__` would be `return self._list.__getitem__[index]` – snakecharmerb Nov 13 '19 at 06:55
  • I think your second link (https://treyhunner.com/2019/04/why-you-shouldnt-inherit-from-list-and-dict-in-python/) has a pretty good summary of the pitfalls, alternatives, pros, and cons. – augurar Nov 13 '19 at 07:41

1 Answers1

2

There are several ways to do it.

  1. Subclass collections.UserList instead of list. This is basically a wrapper around list designed to be extensible.

  2. Subclass collections.abc.MutableSequence and implement all of the required abstract methods. This might be useful if you want to define a totally new sequence-like class.

  3. Subclass list. This might actually be fine in limited scenarios like your TitledList example, where you're just adding a new attribute. But if you want to ovverride list's existing methods then this may not be a good idea.

  4. Create a new object with a list attribute. This is simple and easy to understand, but might be inconvenient. It all depends on your use case.

Reference: Trey Hunner: The problem with inheriting from dict and list in Python

augurar
  • 12,081
  • 6
  • 50
  • 65
  • As I understand subclassing `collections.abc.MutableSequence`, I should implement `__getitem__`, `__setitem__`, `__delitem__`, `__len__`, and `insert`, and I will get "a bunch of stuff" for free from the abstract base class. Where is it documented what I get? I'd like to know what methods the `MutableSequence` comes with, both its own as well as any it inherits from further up the inheritance tree. – thecommexokid Nov 14 '19 at 23:23
  • For instance, commenter @jonathan.scholbach points out in the question comments that `__add__()` poses some immediate problems. Where in the documentation can I find out if `MutableSequence` defines (or inherits) an implementation for `__add__`? – thecommexokid Nov 14 '19 at 23:25
  • 1
    @thecommexokid The `collections.abc` documentation includes a table of which abstract methods are specified by each ABC, as well as "mixin" methods for which the ABC provides a default implementation. – augurar Nov 15 '19 at 00:45