Note: This post is part of a series about OOP in Python. The previous post looked at the class structure in Matplotlib. The next post shows how to use composition and inheritance together in one project.
Inheritance is a natural concept to introduce when teaching and discussing OOP. As powerful as it is, however, it can lead to problematic models of real-world objects and abstract concepts. Many of these issues can be addressed using a concept called composition.
With inheritance, you model complex systems by writing classes that extend the behavior of other classes. Inheritance is a powerful concept, but it can create problems when the classes become too tightly coupled. For example, making changes to an existing class can affect all subclasses, even the ones you have no control over.
When using composition, you write smaller classes representing individual parts of a system. The resulting connections between classes are much less likely to create issues as each part of the system evolves.
A Library
has Books
If you’ve written code that uses OOP there’s a fair chance you’ve used composition before, even if you haven’t heard the term specifically. As an example, consider the context of modeling a library that lends books to its patrons.
The Book
class
Here’s the first part of library.py:
class Book:
def __init__(self, title="", author=""):
self.title = title
self.author = author
def get_info(self):
return f"{self.title}, by {self.author}"
In this simplified example, the library only has books. We write a Book
class that has two attributes: a title, and an author. The method get_info()
returns a string containing both of these attributes.
The Library
class
Now we need a class that represents the actual library:
class Library:
def __init__(self, name="", books=None):
self.name = name
if books is None:
self.books = []
else:
self.books = books
def show_books(self):
print(f"All books in {self.name}:")
for book in self.books:
info = book.get_info()
print(f"- {info}")
When you create an instance of Library
you can give it a name, and initialize the library with a list of books. Calling show_books()
will display the library’s name, and show information about each book in the library’s catalog.1
Making a library
Let’s create a library and some books, and add them to the library’s catalog:
if __name__ == "__main__":
library = Library(name="The Climber's Library")
book = Book(
title="Freedom of the Hills",
author="The Mountaineers"
)
library.books.append(book)
book = Book(
title = "Learning to Fly",
author = "Steph Davis",
)
library.books.append(book)
library.show_books()
We first create an instance of Library
that focuses on books about climbing. We then create two instances of Book
. We call library.books.append()
to add each book to the library’s catalog. Finally, we show the catalog.
Here’s the output:
$ python library.py
All books in The Climber's Library:
- Freedom of the Hills, by The Mountaineers
- Learning to Fly, by Steph Davis
There are only two books in the library’s catalog, but this structure would work for thousands or even millions of books.
A natural use of composition
It’s pretty clear that there’s no use of inheritance in this example. Instead, what we saw here was an example of composition. The Library
class uses instances of Books
. We started building a model of a library by composing a set of independent classes.
There’s a fair chance that if you asked people with a basic understanding of OOP in Python to write their own implementation for this example, they’d come up with a something similar to this. Composition at its core is not something overly complicated. It’s a way of thinking that comes quite naturally in certain situations.
When discussing OOP, we often talk about is an and has a relationships. We can say that a library has a book, but we’d never say that a library is a book. In situations where you’re modeling has a relationships, composition is a natural fit.
Conclusions
You can start to see some of the strengths of composition even in the short example shown here. For example, there are many ways we might expand the Book
class in a real-world implementation. Books can have multiple authors, so the author
attribute might become a list called authors
. There’s a lot of information we might want to store about an author, so we might write an Author
class. If we use instances of Author
in the Book
class, we’d be using composition again.
When building a system using composition, you can often develop the individual classes as much as you need, while having little impact on the rest of the system. This is one of the main strengths of composition.
In the next post we’ll expand this example using a mix of composition and inheritance.
Resources
You can find the code files from this post in the mostly_python GitHub repository.
You might be wondering why the __init__()
method in Library
uses an if
block when assigning a value to self.books
. Briefly, you shouldn’t use an empty list as a default argument. That can cause problems as soon as you have more than one instance of the class.
For a longer discussion of this issue, see MP #20.