LANG·I Language Chapter 12 of 65
The island of rabbits and foxes
A simulation chapter: we build an island where grass feeds the rabbits and rabbits feed the foxes, and watch thousands of small decisions add up to waves of population. Along the way come classes and objects, which were invented in Norway for models like this one.
Language
Builds on: 11 · The inquiry report 08 · Dictionaries and the telegraph
What you will take away
- describe things in the world as classes, with state in attributes and behavior in methods
- build families of classes with inheritance and assemble a large model out of objects
- tell when a class is called for and when a function, a dictionary or a dataclass will do
The last chapter left our code reliable: it checks its input, catches exceptions and tests itself. But everything it has handled so far has been numbers and strings, and the world you want to describe in a program is made of things. A bank account has a balance, and you can’t withdraw more than the balance. A game character has health and a place on the map. A hare has hunger, an age and a burrow. There are thousands of such things, each with its own state, each behaving by its own rules.
We are going to build an island. Grass grows on it, rabbits eat the grass, and foxes eat the rabbits. Every animal lives on its own: it wanders about, spends energy, eats when it is lucky, has young, and starves when its luck runs out. Here is what you will have by the end of the chapter. Run it.
Green is grass, sandy brown is bare earth where the grass has been eaten and has not grown back yet. The gray dots are rabbits, the reddish ones foxes. Not one line of the program is about the population as a whole: nobody orders “let there be lots of rabbits after a hundred days.” Each animal decides only for itself. And yet the waves form on their own: the rabbits multiply, the foxes eat well and multiply too, the rabbits grow scarce, the foxes go hungry, and it all starts over. Watch the numbers in the corners: the foxes keep lagging a few dozen days behind the rabbits.
Almost everything in this program is unfamiliar for now: Island, Rabbit, Fox, the dots after the names. By the end of the chapter you will have written the island yourself, line by line, and you will be able to change anything in it: make the foxes greedier, the grass slower, bring a drought down on the island.
An island of dictionaries
We start with what we already know. An animal is a few values under one name: where it stands and how much energy it has. For that we have the dictionary from Chapter 8, and for what the animal does, the functions from Chapter 5.
It works. But in the last line of the output the rabbit has quietly acquired a second “energy,” thanks to a typo in the key. There will be no traceback: the dictionary has no idea which keys a rabbit should have and which it shouldn’t. Nothing would stop you, either, from putting a string into energy or handing a fox to a function that expects a rabbit.
There is a worse problem. The function step knows about every animal at once: each species gets its own if branch. Add a hedgehog and you have to add a branch to step, then to the function that draws the animals, then to the one that counts them. An animal’s data sit in a dictionary while its behavior is smeared across functions, and every function has to keep every species in mind. With two species you can live with that; with twenty you can’t.
We would like to keep everything about a rabbit in one place, what it is made of and what it can do, so that we can say once, “this is what a rabbit is,” and then make as many rabbits as we like. A way of doing that was invented more than sixty years ago, and for a problem like ours: simulation.
Oslo, 1962. A language for models
Directly or at second hand, Simula’s classes passed into the languages almost everything is written in today: C++, Java, C#, Python. Bjarne Stroustrup, the creator of C++, and James Gosling, the creator of Java, have both named Simula as one of the main sources of their ideas. Our island is the same kind of problem Nygaard had: many participants, each with its own state and its own rules. We will borrow his tool.
A blueprint for a rabbit
We describe the rabbit the way Simula described the participants of a model: with a blueprint.
The word class begins the description of a new kind of object, a class. By convention class names start with a capital letter: Rabbit, Fox, Island. Inside is a function with an odd name, __init__, with two underscores on each side. We never call it ourselves; Python calls it whenever it makes a new rabbit.
The line bunny = Rabbit(3, 5) calls the class as if it were a function. Python makes an empty object and passes it to __init__ as the first argument, under the name self, followed by 3 and 5. __init__ then hangs three names on the new object: self.x, self.y, self.energy. Names tied to an object are its attributes, and you reach them with a dot: bunny.energy. An object made from a class is an instance of that class: bunny and fluffy are two instances of Rabbit, and each has its own attributes.
The last line prints <class '__main__.Rabbit'>. Python now has a new type, our own, on a par with int and str. The word __main__ means “from the main program”: the class is defined in the program itself.
What a rabbit can do
So far our rabbit is the same old dictionary with dots instead of square brackets. Everything changes once the class holds behavior as well.
Functions defined inside a class are called methods. Python reads the call bunny.hop(1, 0) like this: find the function hop in the class of the rabbit bunny and call it with bunny as the first argument. In other words, it is the same as
That is why the first parameter of every method is self: it receives the object in front of the dot. In itself self is an ordinary name and could be called anything, but everyone writes self, and so should you. Press “Steps” in the cell above and go as far as the line self.energy -= 1: in the frame of the method hop the name self points at the rabbit the method was called on, bunny. In the frame of the method eat the same self points at fluffy.
self and what the same line looks like when written through the class.Every object you have worked with so far is built the same way.
int, str and list are classes like our Rabbit, only written in C inside Python itself. The method append from Chapter 6 is a function of the class list, and its self is the list in front of the dot. The number 255 is an instance of the class int, and it has a method that counts how many binary digits it takes to write the number down: eight. Everything you can put in a variable in Python is an object of some class.
What a rabbit looks like
Try printing a rabbit.
Python has no idea what is interesting about a rabbit, so it prints the name of the class and the object’s address in memory, a hexadecimal number that will be different for you. We can teach the rabbit to present itself.
Methods whose names have two underscores on each side are special methods, or “dunder” methods, from “double underscore.” You rarely call them directly: Python calls them itself when something needs doing with the object. print and str() ask the object for __str__, a text for people. A list, a debugger and the function repr() from Chapter 7 ask for __repr__, a representation for programmers. A good __repr__ looks like the code that would create an equal object. If you write only __repr__, print uses it.
There are dozens of special methods, and they are how your objects plug into the language. len(x) calls x.__len__(), a + b calls a.__add__(b), a < b calls a.__lt__(b). Give a class an __add__, and its instances can be added with a plus sign, like numbers. Fractions, dates and vectors work this way; you will write a vector yourself in a task at the end of the chapter.
Shared by all
All rabbits have some things in common. A cell of grass gives each of them the same amount, say 4 units of energy. There is no point in storing that 4 in every rabbit. It can be written in the class itself, without self.
When a rabbit looks up self.FOOD, Python first searches the object’s own attributes, finds nothing and goes on to the class. Change the number in the class and it changes for every rabbit at once, even those already born. Class attributes like this are handy for a model’s settings, and we will make use of them.
One more catch in the spirit of Chapter 2. Rabbits live in burrows, and each burrow has its own list of residents. Before you run the program, place your bet.
Fluffy lives in the north burrow, Snowy in the south one. What does print(north.rabbits) print?
Both of them. The line rabbits = [] sits in the body of the class and runs once, when Python reads the class definition. There is one list, and it lives in the class. The objects north and south have no rabbits attribute of their own, so self.rabbits finds the class’s list, and append changes that one and only list. Two burrows, one list of residents for the whole island.
This is the trap from Chapter 6 again: two labels on one mutable object. The cure is to create the list in __init__. The line self.rabbits = [] runs each time a burrow is created and gives every burrow a fresh list of its own. Hence the rule: mutable values (lists, dictionaries, sets) are created in __init__, and the class body holds only what is shared and immutable, such as numbers used as settings.
Two rabbits or one
When are two rabbits equal?
Rabbits a and b stand in the same cell with the same energy, and still a == b is false. For classes of your own, Python by default treats an object as equal only to itself: == works like is. For animals that is right, since two rabbits in one cell are still two rabbits. And c = a copies nothing; it ties a second label to the rabbit, as in Chapter 2. A fox catches c, and a is dead, because they are one and the same rabbit. Walk through the program with “Steps”: the arrows from a and c lead to the same place.
Some objects are naturally compared by what is in them: a cell of the island, a point, a vector, a date. For those you write __eq__.
Now two cells with the same coordinates are equal, and in finds a cell in a list, because it compares the items with ==. But the last line fails with TypeError: unhashable type: 'Cell'. It is the same story as with dictionary keys in Chapter 8: a set files its items away in drawers, and for an object whose equality you have defined yourself, Python doesn’t know how to choose a drawer so that equal cells land in the same one. That takes one more method, __hash__. How it works, and why such objects had better never change, is the subject of Chapter 16.
A fence around the grass
A rabbit eats grass. Where is the grass kept? Let the island remember, for every cell, how many days are left until the grass grows back: 0 means it has grown, 10 that it has just been eaten. Here is the meadow.
The table _wait starts with an underscore. To Python that is an ordinary name, but among programmers it is a convention meaning “this is internal, keep out.” The rabbit will not climb into the table and subtract ones from it. It asks the meadow, meadow.has_grass(x, y), and tells it, meadow.eat(x, y). Whatever the meadow can do, it does itself, through its own methods.
The strictness pays off: the meadow can now be rebuilt without touching the rabbits. If tomorrow we decide to store only the eaten cells, in a dictionary of “cell → days to wait,” the inside of Meadow changes, and the rabbits, which know only has_grass and eat, are none the wiser. An object that hides its workings behind its methods and enforces its own rules is said to use encapsulation.
Python puts no locks on anything: you can read meadow._wait if you like. The underscore is a waist-high fence that anyone determined enough can step over. But everyone can see where it stands. Banks work on the same principle: an account’s balance is changed only by the methods “deposit” and “withdraw,” and they check that the amount is positive and that there is enough money. You will write such an account, with the checks and exceptions of Chapter 11, in a task.
The fox: inheritance
Time to bring in the foxes. A fox has a lot in common with a rabbit: it stands in a cell, wanders, spends energy and dies when its energy runs out. It differs in what it eats, and perhaps in a habit or two. You could copy the class Rabbit, rename it and adjust it. But then a bug found in one copy would stay in the other, and every improvement would have to be made twice. It is better to move what they share into a class for animals in general and say that the rabbit and the fox are animals, each with its own quirks.
The form class Rabbit(Animal) means that a rabbit is an animal: it has everything Animal has, plus whatever we add. This is inheritance. Animal is the parent class, or base class, and Rabbit and Fox are its subclasses. The rabbit has nothing of its own yet; the word pass means “do nothing.” But it can already do everything: move, live, show itself through repr. In Simula inheritance was written as a prefix, Animal class Fox, “the class Fox with the prefix Animal,” and subclasses themselves were one of the main novelties of Simula 67.
The fox replaces two of its parent’s methods with its own. When a subclass has a method of the same name, that method wins, and the parent’s version can still be called through super(): super().__init__(…) means “do what any animal does when it is born,” super().step() means “wander the way any animal does.” The method __repr__ is written once, in Animal, yet it prints the name of the object’s own class: for a fox, type(self).__name__ is "Fox". And isinstance tells you whether an object belongs to a class or to one of its descendants: the fox is an instance of Fox and of Animal, but not of Rabbit.
You have written inheritance before without knowing the word. In the last chapter the line class AlignmentError(ValueError) declared an exception that is a kind of ValueError, which is why except ValueError caught it. All of Python’s exceptions form a tree like this one: ZeroDivisionError is a special case of ArithmeticError, and that in turn of Exception.
Python finds the method to call by searching along a chain: first the object itself, then its class, then the parent, then the parent’s parent, all the way up to the class object, the common ancestor of every class in Python. The first match wins.
FOOD of its own, and look up a name that exists nowhere.The whole island
All the parts are ready, and the island can be assembled. The island is neither an animal nor grass; it contains them. The island has a meadow and has a list of animals. “A fox is an animal” is inheritance; “the island has a meadow” is object composition. Large programs are put together out of objects the way a car is put together out of parts: an engine is not a kind of car; it goes inside one.
Here is the whole island. It is the longest program in the course so far, but nothing in it is new to you. Read it from top to bottom and run it.
Every day an animal spends one unit of energy, steps into a random neighboring cell unless it is sea, and with a small probability gives birth. The baby gets half its mother’s energy, and its species is decided by the line type(self)(…): “call the class of whoever is giving birth.” That is why give_birth is written once for everyone: a rabbit gives birth to a rabbit, a fox to a fox. On top of that, a rabbit eats the grass under its feet, and a fox eats a rabbit if one is standing in its cell. The numbers FOOD and BIRTH are class attributes, different for each species: self.BIRTH in the method Animal.step finds 0.04 for a rabbit and 0.05 for a fox.
In Island.step the island shuffles the animals, so that nobody always goes first, and says the same thing to each of them: animal.step(self). The island doesn’t know who is in front of it and doesn’t ask; there isn’t a single if about species in that loop. At those words a rabbit goes off to eat grass and a fox goes hunting. One and the same line of code does different things depending on which object receives it. This is called polymorphism, from the Greek for “many forms.” Compare it with the function step from the section “An island of dictionaries”: there the chain of ifs on species would grow with every new animal, while here each new animal brings its behavior along with it.
Below is a diagram of the classes in this program. A box is a class, with its name, attributes and methods. An arrow with a triangle means “is a” (inheritance: a fox is an animal); a line with a diamond means “has a” (composition: the island has a meadow). Add class Hedgehog(Animal): pass to the cell, and a hedgehog appears on the diagram.
There is no reason to haul a hundred-odd lines from cell to cell, so we have put these classes, unchanged, into the module cs.island of the course sandbox, together with a function draw that draws the island frame by frame. Now go back to the first cell of the chapter: there isn’t a word in it you don’t know.
Waves
In an animation a wave is easy to miss; on a chart it shows at once. We will live on the island for a thousand days and write down how many rabbits and foxes there are each day. On the right are the same numbers with the populations themselves on the axes: every point is one day, rabbits across and foxes up.
On the left there are four or five waves, each a little over two hundred days long, and every time the foxes peak later than the rabbits. On the right the waves fold into loops: the populations go around counterclockwise, from many rabbits and few foxes to many of both, then few rabbits and many foxes, then few of both. Run the cell several times. Each island lives through a history of its own, but all the histories have the same shape.
Foxes multiply when rabbits are plentiful, but with a delay: it takes them time to grow numerous. By the time there are many foxes, there are no longer enough rabbits to go around, because the foxes have eaten them. Then the foxes go hungry, and while they are few, the rabbits breed again. The predator’s lag behind its prey is what turns the chase into a seesaw.
The same seesaw was derived from equations in the 1920s by Alfred Lotka and Vito Volterra. Their model has no individual animals, only population sizes, as continuous quantities: hares increase at a rate proportional to their number, minus their encounters with wolves; wolves increase at a rate proportional to the encounters, minus deaths from hunger. The solutions of these equations run around closed loops, like our right-hand plot without the random jitter. The “wolves and hares” bench in the chapter on differential equations of the math course shows how the model is derived and why the loops close. We reached the same picture from the other side: not a single equation, only rules for each animal.
Below is the same island, only it lives right in your browser, so experiments with it go fast. What if the foxes get greedier? What if the grass takes twice as long to grow back? Are there settings at which everyone dies?
cs.island. On the left is the island, on the right the populations by day and the rabbits–foxes loop. The sliders change the class attributes as it runs.The experiments show how fragile the seesaw is. Foxes that hunt too well eat every rabbit and then die out themselves. Foxes that hunt too poorly die out at once, and the rabbits are held back by nothing but the grass. On a small island extinctions happen more often than on a large one: when only five foxes are left, a few unlucky days are enough. Ecologists know this without any simulation; it is how species disappear from actual islands.
Drought: anything that can walk
At the start of the chapter you were promised a drought: for a few weeks the sun scorches part of the grass every day. A drought is not an animal, but then the island doesn’t ask much of its inhabitants. It tells each of them step and asks each is_alive. If a drought can do the same, the island will take it in.
The class Drought inherits from nothing, and the island knows nothing about it. But a drought has step and is_alive, and that is enough: for forty days the island calls it along with the animals, and then it “dies” and drops off the list. It scorches the grass through the same method eat that the rabbits use, so the fence around the meadow holds. The rabbits go hungry first, the foxes after them. Sometimes the foxes don’t survive the drought at all, and then the rabbits recover and breed, free of enemies, right up to the ceiling the grass allows. Run it a few times.
Everything in Python works this way: an object is judged by what it can do, not by its class. If it walks like a duck and quacks like a duck, you may treat it as a duck. The principle has a name to match: duck typing. A for loop works with any object that can hand out items one at a time, len with any object that has __len__, and our island with anything that has step and is_alive.
Our island follows Kay’s recipe. The animals and the drought are small independent computers, each with its own state; the island sends each of them the message step, and each answers in its own way. Which object answers is settled by Python only at the moment of the call. Many years later Kay summed up what objects meant to him: “OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.” Messages, state hidden inside the object, and deciding who answers as late as possible: you have just watched all three at work.
When you don’t need a class
Once you have mastered classes, it is tempting to turn everything into one. At PyCon 2012 Jack Diederich gave a talk called “Stop Writing Classes.” One of his points: if a class has two methods and one of them is __init__, you probably meant to write a function. A class earns its keep when a thing has state that changes, rules that must be enforced and behavior that varies with the kind of thing, as with our animals. When none of that is there, simpler tools will do.
- A function, when there is an action but no state to keep between calls. Converting degrees to radians is a function, not a class called Converter.
- A tuple or a dictionary, when several values need to be bundled together and passed along: a row of the earthquake catalog from Chapter 6, a JSON record in Chapter 8.
- A dataclass, when you want to name the data and compare it, but writing
__init__,__repr__and__eq__feels like a chore.
The line @dataclass above the class is a decorator: a function that takes the class and writes __init__, __repr__ and __eq__ for it from the list of fields. The dataclasses module arrived in Python 3.7, in 2018; it was proposed by Eric Smith, the same person who came up with the f-strings of Chapter 2. Our cell from the section on equality fits into four lines.
Tasks
Four tasks, from a single class to a family of classes. The tests create your objects, call their methods and check the state after every step, including that two objects don’t share one list.
Write a class Rabbit. A rabbit is created with a name, Rabbit("Fluffy"), and has the attributes name, energy (10 to begin with) and hops (how many times it has hopped, 0 to begin with). Its methods:
eat(food)addsfoodto the energy;hop()makes a hop, which costs one unit of energy and adds one tohops. If the energy is already 0, the rabbit doesn’t hop and nothing changes. The method returnsTrueif the hop happened andFalseif it didn’t;is_alive()is true as long as the energy is above zero;__repr__returns a string likeRabbit('Fluffy', energy=10).
In __init__, hang three attributes on self: self.name = name, self.energy = 10, self.hops = 0.
In hop, check the energy first: if self.energy == 0: return False. For __repr__, !r in an f-string comes in handy: f"{self.name!r}" puts the name in quotes, as repr does.
The check at the start of hop keeps the energy from going below zero. As long as the rabbit is changed only through its methods, nobody outside can break that rule: this is encapsulation at work.
Write a class Vector, a vector on the plane, like the move an animal makes in one step. Vector(3, 4) stores the attributes x and y. Teach vectors to speak the language:
v + wandv - wgive a new vector, andvandwthemselves don’t change;v * 3and3 * vgive the vector stretched three times;v == wis true when the coordinates match; comparing with something that isn’t a vector givesFalse, not an error;v.length()is the length, $\sqrt{x^2 + y^2}$;repr(Vector(3, 4))is the stringVector(3, 4).
The special methods you need are __add__, __sub__, __mul__, __eq__ and __repr__. Each arithmetic one returns a new Vector(…).
v * 3 calls v.__mul__(3). But 3 * v asks the number first: int.__mul__(3, v). A number can’t multiply itself by a vector, so Python then tries the vector’s “right-hand” multiplication, v.__rmul__(3). Write an __rmul__ that does the same as __mul__.
In __eq__, check isinstance(other, Vector) first: the tuple (3, 4) has no attribute x, and without the check the comparison crashes.
math.hypot(x, y) computes $\sqrt{x^2+y^2}$ more carefully than the formula written out by hand: it doesn’t overflow on huge numbers. Vectors that add with a plus sign show how special methods make your class part of the language; fractions.Fraction, datetime and NumPy arrays are built the same way.
Write a class Account. Account("Ada") is an account with a balance of zero, Account("Ada", 100) one with a hundred. Its attributes are owner, balance and history, a list of operations stored as tuples ("deposit", 50) and ("withdraw", 30). Its methods:
deposit(amount)puts money in. If the amount is not greater than zero, it raisesValueError;withdraw(amount)takes money out. If the amount is not greater than zero or is more than the balance, it raisesValueError;transfer(other, amount)moves money to the accountother, withdrawing it here and depositing it there.
After a failed operation the balance and the history must stay as they were. And each account keeps a history of its own.
The starter code already holds the trap from the section about the burrows. Find it: create two accounts and put money into one of them.
Check the amount before you change anything: if amount <= 0: raise ValueError("the amount must be greater than zero"). Then nothing has changed yet when the error strikes.
In transfer, call self.withdraw(amount) first: if there isn’t enough money, it raises an exception, and other.deposit is never reached.
The list history = [] in the body of the class was shared by all the accounts, so Ada’s operations landed in Grace’s history. The rule “check first, then change” makes every operation indivisible: it either goes through completely or changes nothing at all. Banks get the same guarantee from database transactions, which are the subject of Chapter 46.
Build a family of classes. A base class Shape has the methods area() and perimeter(), which raise NotImplementedError: a shape in general has no area, only a particular shape does. It also has a method describe(), written once, in Shape, which returns a string like "Circle: area 3.14": the name of the class and the area to two decimal places.
The subclasses are Circle(r), Rect(w, h) and Square(side). A square is a rectangle, so Square inherits from Rect and rewrites neither area nor perimeter. Also write a function largest(shapes) that returns the shape with the largest area (None for an empty list).
describe doesn’t know which shape it is dealing with, and it doesn’t need to: type(self).__name__ gives the name of the class, and self.area() calls the area of the right subclass. Two decimal places are f"{x:.2f}".
The square needs only one method: def __init__(self, side): super().__init__(side, side). Everything else it gets from the rectangle.
For largest, remember max with a key from Chapter 10: max(shapes, key=lambda s: s.area()).
The method describe and the function largest are written once and work with any shape, even shapes that don’t exist yet: add a Triangle with its own area and perimeter, and nothing else needs to change. Polymorphism is doing the work for you. And the square, which rewrites nothing, shows what inheritance is good for: it got the rectangle’s formulas for free.
What next
Our island is 48 by 30 cells, with a couple of hundred animals on it. Ecologists model forests and oceans with millions of individuals. Suppose we make the island bigger and populate it more densely. The model stays the same; we only time how long one day takes.
Each time there are twice as many animals, and a day takes nearly four times as long. With sixteen thousand rabbits a day lasts almost a second, and a thousand days more than ten minutes. A hundred thousand rabbits are beyond this island altogether: judging by the growth, a single day would take about half a minute. Somewhere in our tidy code hides something that grows faster than the number of animals. Where is it? And how can you predict a program’s running time in advance, from its text, before you run it? In 2021 a similar question occurred to a player who was so fed up with a game taking six minutes to load that he dug into its machine code. His investigation opens the next chapter, and the second part of the course.