Composition

Another key part of object-oriented programming is composition: an object can be composed of other objects. In Python, since every piece of data (number, boolean, string, etc.) is an object, technically every example we've seen so far has been an example of composition. However, I want to dive into examples where an object of a user-defined class contains references to other objects of user-defined classes, since that's more interesting.

Going back to our animal conservatory, what examples of composition could there be? A few ideas:

We can implement all of those examples using instance variables that either store a single object or a list of objects.

Composition with a single object

Here, let's code a method for mating one animal with another animal:

class Animal:

    def mate_with(self, other):
        if other is not self and other.species_name == self.species_name:
            self.mate = other
            other.mate = self

That method first checks to make sure we're not trying to mate an animal with itself, and also ensures the animals are the same species, since we want the best for their reproductive future. If that all checks out, then it sets a new instance variable mate to the other animal object passed in, and also sets the mate instance variable of the other animal to itself. That sets up a symmetric relationship where both objects are pointing at each other.

Here's how we call that method:

mr_wabbit = Rabbit("Mister Wabbit", 3)
jane_doe = Rabbit("Jane Doe", 2)
mr_wabbit.mate_with(jane_doe)

Composition with a list

An instance variable could also store a list of objects.

Read through this code that simulates rabbit reproduction:

class Rabbit(Animal):

    # Other methods/class variables omitted for brevity

    def reproduce_like_rabbits(self):
        if self.mate is None:
            print("oh no! better go on ZoOkCupid")
            return
        self.babies = []
        for _ in range(self.num_in_litter):
            self.babies.append(Rabbit("bunny", 0))

It first makes sure the rabbit has a mate, since rabbits don't reproduce asexually. It then initializes the babies instance variable to an empty list, and uses loop to add a bunch of new rabbits to that list with an age of 0.

We can call that method after setting up the mate relationship:

mr_wabbit = Rabbit("Mister Wabbit", 3)
jane_doe = Rabbit("Jane Doe", 2)
mr_wabbit.mate_with(jane_doe)
jane_doe.reproduce_like_rabbits()

This composition isn't perfect: a better approach might be to initialize babies inside __init__, so that a rabbit could reproduce multiple times and keep growing its list of babies each time. It also might be a good thing to add a line like self.mate.babies = self.babies or self.mate.babies.extend(self.babies) so that the mate could also keep track of the produced babies. The best implementation really depends on the use case for the compositional relationship and how the rest of the program will use that information.

Inheritance vs. composition

You've now learned two ways that classes and objects can relate to each other, composition and inheritance. Let's compare them.

Inheritance is best for representing "is-a" relationships.

Composition is best for representing "has-a" relationships.

When you're designing a system of classes, keep in mind how objects relate to each other so you can figure out the most appropriate way to represent that in code. Sometimes it helps to draw before you code, sketching the relationships out on paper or making a formal diagram using UML (Unified Modeling Language).

➡️ Next up: Polymorphism