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:
An animal has a mate (another animal of the same class).
An animal has a mother (also another animal of the same class).
An animal has children (multiple animals of the same class).
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.
A rabbit is a specific type of animal.
So, the
Rabbit
class inherits fromAnimal
.
Composition is best for representing "has-a" relationships.
A rabbit has a litter of babies.
So, a
Rabbit
object has an instance variable containing otherRabbit
objects.
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).