OOP in Python, part 18: Composition and inheritance

MP 64: A mix of the two approaches is often the most practical solution.

Note: This post is part of a series about OOP in Python. The previous post introduced composition. The next post discusses when to use OOP, and when OOP is not needed.


In the last post we discussed how composition can be used to model has a relationships, such as a library that has a collection of books. But what happens when there are more relationships in the overall scenario you’re trying to model? What if you have a mix of has a and is a relationships?

In real-world situations you often need to use a mix of inheritance and composition. In this post we’ll extend the library example to include a mix of inheritance where appropriate, and composition where appropriate.

Books and magazines

A library that only lends books is a little limiting. Let’s expand the library to include a collection of magazines as well.

To do this, we need to think about two kinds of relationships:

  • What’s the relationship between a library and a magazine?
  • What’s the relationship between a magazine and a book?

If the word relationship isn’t helping you think clearly about this, try using the word connection instead.

Libraries and magazines

The relationship (or connection) between a library and a magazine is easier to think about, so let’s start there. A library has a magazine, or a collection of magazines. That steers us toward composition—the Library class will have a collection of Magazine instances.

cover of Alpinist 78, showing climber on snowy mountain ridge
I’ve spent many enjoyable afternoons at libraries over the years, browsing the archives of climbing magazines such as Alpinist.
Books and magazines: conceptual thinking

The relationship between books and magazines is less clear. A book doesn’t have a magazine, and a magazine doesn’t have a book. So we’re probably not going to be using composition between them.1

Let’s consider the is a relationship. A book is not a magazine, and a magazine is not a book. They seem related somehow though, especially in the context of a library. We’ll do some common things with them. We’ll catalog them, we’ll loan them out, and people will return them. So they’ll have some common attributes and behaviors, which leads us towards inheritance. Is there any way we can fill in the following blanks with the same word?

  • A book is a _____.
  • A magazine is a _____.

I’ll pause a moment here before sharing my thoughts on how to approach this. If you can come up with a word that works for both of these examples, you could probably write code to match your mental model. Even if your model is different than mine, you’d be on your way toward a reasonable implementation of a library.

Two words come to mind for me: publication and resource. I was inclined to go with publication because I’ve been thinking about what books and magazines are. But as I write all this out I find myself thinking more about what a library is, and what a book and a magazine mean to a library. In this context, I think resource might be the better term. We loan and catalog resources, and this line of thinking will extend to a number of other things: CDs, DVDs, board games, headphones, and more.

Also, it’s worth noting before we write any code that we’re not committing ourselves too much by choosing a specific way of thinking at this point. If we choose publication as the parent of books and magazines, we can always go back and insert another layer in the hierarchy called resource. If we start with resource, we can always go back and add a publication layer into the hierarchy. If you recognize the need for these layers right away, it’s easier to put them in now. But you can also refactor these layers in later if the need arises.

Books and magazines: representation in code

I’m considering books and magazines to both be examples of library resources, so let’s write a Resource class. It will contain the information that’s common to every resource a library might have, and behavior that’s common to all resources as well.

Here’s a first pass at writing the Resource class. We’re working with a number of classes, so let’s put all these in a file that will only contain the classes. We’ll call it library_models.py:

class Resource:
    def __init__(self, resource_type):
        self.resource_type = resource_type
        self.current_borrower = None

    def lend(self, patron):
        self.current_borrower = patron

    def return_resource(self):
        self.current_borrower = None

This class defines two attributes: what kind of resource each instance is, and a reference to a borrower. When you create a resource, it’s not checked out to anyone initially.

The Resource class needs a lend() method; writing the lend() method made me realize we need a model for the library’s patrons. The lend() method sets the value of current_borrower to a specific patron. The return_resource() method sets the value of current_borrower back to None. You might have noticed a discrepancy between the method names here. We can’t have a function named return(), because that would override the built-in return statement.

Here’s the Patron class:

class Patron:
    def __init__(self, name):
        self.name = name

Now we can update the Book class. It’s mostly the same as the version we used in the last post, except it inherits from Resource:

class Book(Resource):
    def __init__(self, title, author):
        self.title = title
        self.author = author
        super().__init__(resource_type="book")

    def get_info(self):
        return f"{self.title}, by {self.author}"

This new version only needs to pass a resource_type argument to the parent __init__() method.

The show_status() method

As I was updating Book, I started to add code to the get_info() method that would show whether the book was available or not. Then I realized this functionality should apply to all resources. So I added a get_status() method to Resource:

class Resource:
    ...
    def get_status(self):
        if self.current_borrower:
            return "on loan"
        else:
            return "available"

If there’s a value set for current_borrower, the resource is on loan to someone. Otherwise, it’s available. Even in a small project like the example library we’re building, I think it’s important to show that not all code and functionality is clear from the start. Often times code for one part of a project becomes clear as you’re working on a different part of the project.

The updated Library class

The Library class has the same functionality as it did in the last post, but it’s expanded to work with various resources, not just books:

class Library:
    def __init__(self, name, resources=None):
        self.name = name

        if resources is None:
            self.resources = []
        else:
            self.resources = resources

    def get_books(self):
        books = [
            r for r in self.resources
            if r.resource_type == "book"
        ]
        return books

    def show_books(self):
        print(f"All books in {self.name}:")
        for book in self.get_books():
            info = book.get_info()
            print(f"- {info} ({book.get_status()})")

A library has a collection of resources now, rather than just a collection of books. When you want to work with only books, you can call the method get_books(). This method uses a comprehension to generate a list of resources that are designated as books.

The method show_books() now calls get_books() when iterating over the collection of books. It also adds a note about whether the book is currently available, or out on loan.

Making a library

With these classes defined, we can build a library. We’ll do this in a separate file called climbing_library.py:

from library_models import Library, Book, Patron

# Create a library.
library = Library(name="The Climber's Library")

# Create some books.
book = Book(
    title="Freedom of the Hills",
    author="The Mountaineers",
    )
library.resources.append(book)

book = Book(
    title="Learning to Fly",
    author="Steph Davis",
)
library.resources.append(book)

# Show all books.
library.show_books()

Before adding patrons and magazines, let’s make sure the functionality we had in the last post still works. Here we create a library, and then create two books just as we did before. Notice that the code for creating an instance of Book hasn’t changed. The fact that Book inherits from Resource is an implementation detail, invisible to the end user.

We import the necessary classes, and append to library.resources instead of library.books.2

The output is the same as it was in the last post, with the additional information about whether each book is available or not:

$ python climbing_library.py 
All books in The Climber's Library:
- Freedom of the Hills, by The Mountaineers (available)
- Learning to Fly, by Steph Davis (available)

This is good; it means the new functionality we’ve introduced hasn’t broken any existing functionality.

Adding patrons and borrowing books

Now we can make a patron, loan them a book, and verify that the book is on loan:

...
# Show all books.
library.show_books()

# Lend a book.
birdie = Patron("Birdie")
freedom_hills = library.get_books()[0]
freedom_hills.lend(birdie)

# Show all books.
library.show_books()

We create a patron named Birdie. We pull out the first book in the collection, and call its lend() method. We then show all the books in the library again:

...
All books in The Climber's Library:
- Freedom of the Hills, by The Mountaineers (on loan)
- Learning to Fly, by Steph Davis (available)

The second listing of books shows that Freedom of the Hills is currently out on loan. The system is working. :)

Loaning books and magazines

Now we can create some magazines as well as some books. Here’s the Magazine class:

class Magazine(Resource):
    def __init__(self, title, issue):
        self.title = title
        self.issue = issue
        super().__init__(resource_type="magazine")

    def get_info(self):
        return f"{self.title}, issue {self.issue}"

This is structured just like Book. All this code belongs in Magazine and not Resource, because the information and format is specific to a magazine.

Now we need to update the Library class to work with magazines:

class Library:
    ...
    def get_magazines(self):
        magazines = [
            r for r in self.resources
            if r.resource_type == "magazine"
        ]
        return magazines

    def show_magazines(self):
        print(f"All magazines in {self.name}:")
        for magazine in self.get_magazines():
            info = magazine.get_info()
            print(f"- {info} ({magazine.get_status()})")

We add two methods to Library, to get all the magazines in the library’s collection and to show them.3

Now we can add some magazines to the library’s resources, and loan one out:

...
# Show all books.
library.show_books()

# Create some magazines.
for issue_num in range(70, 84):
    magazine = Magazine(title="Alpinist", issue=issue_num)
    library.resources.append(magazine)

# Loan the latest issue.
current_issue = library.get_magazines()[-1]
current_issue.lend(birdie)

# Show magazines.
library.show_magazines()

We create the most recent issues of Alpinist magazine, and add them to library.resources. We then get the current issue, and lend it to birdie. Finally, we show the state of the magazine collection.

This works, just as it does for books:

$ python climbing_library.py
...
All magazines in The Climber's Library:
- Alpinist, issue 70 (available)
- Alpinist, issue 71 (available)
  ...
- Alpinist, issue 82 (available)
- Alpinist, issue 83 (on loan)

All the issues of the magazine show up, and the latest issue is currently on loan.

Conclusions

In most real-world projects, a practical implementation won’t involve purely inheritance or purely composition. Many times you’ll need to mix the two approaches to come up with a solution that makes sense for what you’re trying to accomplish.

If we were writing code for a real-world library project, there are a number of ways we’d extend the ideas that were introduced here:

  • Information about the library’s resources would be stored in a database. We’d still have OOP code that acts on the data we’re currently working with.
  • We could make Resource an abstract base class. People should make instances of specific resource subclasses, but in the current implementation there’s no reason to make an instance of Resource. We could then define specific methods that all resource types need to implement.
  • We’d consider building out methods that might seem redundant, but would create a more natural and intuitive API. For example we currently have resource.lend(patron), but it might be helpful to have patron.borrow(resource) as well. The functionality of lending resources already exists, but additional methods would support multiple ways of conceptualizing the relationship between patrons and resources. The implementation would be simple: patron.borrow() would be a one-line wrapper around resource.lend().
  • The lend() method would set a due date. Due dates are central to libraries, so we might write our own class to represent them. The class could have methods like extend(), and have properties like overdue.

Throughout all this work, a clear understanding of composition and inheritance would help you build a model that’s understandable, flexible, and maintainable. A reasonable implementation would allow you to extend individual parts of the system, without too much concern that your new work will negatively impact existing functionality.

Resources

You can find the code files from this post in the mostly_python GitHub repository.


  1. You might recognize at this point that a magazine does have articles. If we extend the model further we might want to use composition to connect Magazine instances with Article instances.

  2. A more complete implementation of the Library class would probably include an add_book() method. Again, the change making Book a subclass of Resource would be invisible to end users.

  3. We now have two pairs of similar methods: get_books() and show_books(), and get_magazines() and show_magazines(). These could be refactored into two more general methods, get_resources() and show_resources(). The method get_resources() would require a resource_type argument, and then it would return resources of that specific type.

    If you do this refactoring work, you wouldn’t need to add two new methods every time the library adds a new type of resource to its collection.