Skip to content

← Grade 11 Computing

Objects and Classes

Unit 1 · Algorithms and programming

Hide the stories, facts, photos, activities and interactives, for revising before an exam
In this unit 11 sections
  1. Things that know and do
  2. Classes: one design, many objects
  3. Drawing a class diagram
  4. Keeping an object’s data right
  5. Objects that hold other objects
  6. A kind of: inheritance
  7. A simulation built from objects
  8. Objects or not?
  9. Project: a program built from objects
  10. In short
  11. Exercise

Stand at a bus stop in Lagankhel and watch. A big green bus pulls in, then a micro, then another bus. Each has its own number plate, its own number of seats and its own crowd of people inside. Yet they all do the same things: stop, let people off, let people on, drive to the next stop. If you were writing a program about buses, you would want to say once what every bus is like, and then make as many buses as you need, each keeping its own numbers.

That is what this unit is about. Until now your programs have kept their data in one place (variables, lists, dictionaries) and the code that works on it in another (functions). In this unit you bring the two together. You will make objects: things in a program that know facts about themselves and can do things. You will write classes, the designs that objects are made from, draw them as diagrams before you code them, keep each object’s data correct, build objects that hold other objects, make one class from another, and build a simulation and a game out of objects. Most of the large programs in the world, from a phone’s apps to the games on it, are built this way.

A long green and white bus with Sajha Yatayat written on its side in Nepali and English, parked on a road in front of shops
Figure 1.1 A Sajha Yatayat bus in the Kathmandu Valley. Sajha Yatayat, a cooperative, first ran buses there in 1962. In a program, every bus like this one can be an object. Photo: Krish Dulal, CC BY-SA 3.0

Things that know and do

Think about one bus and ask two questions.

  • What does it know? Its number plate, how many seats it has, how many people are on board now, which route it runs.
  • What can it do? Let people on (if there is room), let people off, move to the next stop.

Almost anything a program deals with can be described this way. A student knows a name, a class and their marks, and can sit an exam. A savings account at a cooperative knows its owner and its balance, and can take a deposit or pay out a withdrawal. A tiger in Bagh-Chal knows where it stands on the board, and can move or jump.

The way you know: a dictionary and some functions

With what you learned in Grades 9 and 10, you would keep each bus in a dictionary, and write functions that are given a bus:

def new_bus(plate, seats):    """A bus as a dictionary."""    return {"plate": plate, "seats": seats, "riders": 0} def board(bus, n):    """n people get on, if there is room."""    if bus["riders"] + n <= bus["seats"]:        bus["riders"] = bus["riders"] + n sajha = new_bus("Ba 2 Kha 4512", 40)board(sajha, 25)sajha["rider"] = 50sajha["riders"] = -3print(sajha)

Output

{'plate': 'Ba 2 Kha 4512', 'seats': 40, 'riders': -3, 'rider': 50}

It works, but look at the last two changes. A line far away from board() set the number of riders to −3, which is impossible, and nothing stopped it. Another line misspelled a key, "rider", and instead of an error Python quietly made a new key. In a program of a few thousand lines, written by several people, these mistakes happen, and they are hard to find. There is also nothing that says board() belongs with buses: it is just one function among hundreds, and nothing stops someone calling it with a student’s dictionary.

Data and actions together: objects

An object keeps both together: the facts it knows and the actions it can do. In Python:

  • The facts an object keeps are its attributes. A bus object has the attributes plate, seats and riders, each holding a value.
  • The actions an object can do are its methods: functions that belong to the object and work on its own attributes. A bus has the methods board() and alight().

You reach both with a dot: sajha.plate is the bus’s number plate, and sajha.board(25) asks that bus to let 25 people on.

You have been using objects all along

This is not as new as it sounds. Every value in Python is an object. In Grade 8, type() told you that a value’s type was <class 'int'> or <class 'str'>; in Grade 9 you called methods on strings and lists: "momo".upper(), orders.append("tea"). A string is an object of the class str, and upper() is one of its methods. Each list is an object of the class list with its own items, and append() changes that list and no other.

print(type("momo"), type([1, 2]), type(5))print("momo".upper(), str.upper("momo"))

Output

<class 'str'> <class 'list'> <class 'int'>MOMO MOMO

The second line shows something worth remembering: "momo".upper() is a short way of writing str.upper("momo"), the class’s method given the object to work on. Hold on to that; it explains self, in the next section.

What is new in this unit is that you will write classes of your own: a Bus, a Passenger, an Account, a Tiger.

In a program about a school, which of these is best written as a method of a Student object, rather than an attribute?

Pick an answer to see why.

Classes: one design, many objects

Before a bus company buys forty buses, someone has designed the bus: where the seats go, how many there are, where the doors are. The design is not a bus; you can’t ride in it. But every bus built from it has seats and doors, each of them its own.

In a program, the design is a class, and each thing made from it is an object of that class (also called an instance of it). The class says what attributes every object will have and what methods it can do. Each object has its own values for the attributes.

Your first class

Here is the bus as a class. Read it, then press Run.

class Bus:    """A bus with a number plate, a number of seats and the people on board."""     def __init__(self, plate, seats):        self.plate = plate        self.seats = seats        self.riders = 0     def board(self, n):        """n people get on, as many as there are seats for. Returns how many got on."""        room = self.seats - self.riders        got_on = min(n, room)        self.riders = self.riders + got_on        return got_on     def alight(self, n):        """n people get off (never more than are on board)."""        self.riders = max(0, self.riders - n)     def __str__(self):        return f"{self.plate}: {self.riders} of {self.seats} seats taken"  sajha = Bus("Ba 2 Kha 4512", 40)micro = Bus("Ba 3 Ja 118", 14) sajha.board(25)print(micro.board(18), "got on the micro")micro.alight(5)print(sajha)print(micro)

Output

14 got on the microBa 2 Kha 4512: 25 of 40 seats takenBa 3 Ja 118: 9 of 14 seats taken

Take it a piece at a time.

  • class Bus: starts the class and names it. Class names start with a capital letter, by habit, so you can tell Bus (the design) from bus (a variable). The lines indented under it belong to the class.
  • The methods are written inside the class with def, like functions. Every method’s first parameter is self, explained below.
  • __init__ (two underscores on each side; say “dunder init”) is a special method: Python calls it by itself each time a new object is made, to give the new object its attributes. A method like this, which sets up a new object, is called a constructor.
  • sajha = Bus("Ba 2 Kha 4512", 40) makes an object. Calling the class like a function does three things: Python makes a new, empty Bus object; it calls __init__ with that object as self and your values as plate and seats; then it gives you the object, and sajha points to it.
  • self.plate = plate gives the object an attribute. plate alone is __init__’s parameter, gone when __init__ ends; self.plate is kept on the object for as long as the object lasts.

What self is

When you write sajha.board(25), Python turns it into Bus.board(sajha, 25), just as "momo".upper() was str.upper("momo"). The object before the dot goes in as the method’s first argument. So inside board, self is whichever bus the method was called on: sajha for sajha.board(25), micro for micro.board(18). That is how one method, written once, works on every bus and changes only the right one.

Step through it and watch the arrows.

Try it

One class, two buses

This program uses a shorter Bus, whose board() doesn’t check the seats. Press Next line. The names are on the left, in boxes as in Grade 10: the program’s names at the bottom, each running call’s names above them. The objects are on the right. A name that refers to an object has an arrow to it; self’s arrow is red.

The program’s names

Bus 
(a class) 
 
 
 
 

Objects

Step 1 / 14

Line 1: class Bus:

class Bus: makes the class, the design every bus will follow, and keeps it under the name Bus. The defs inside it make its methods. No bus exists yet.

class Bus:    def __init__(self, plate, seats):        self.plate = plate        self.seats = seats        self.riders = 0     def board(self, n):        self.riders = self.riders + n sajha = Bus("Ba 2 Kha 4512", 40)micro = Bus("Ba 3 Ja 118", 14)sajha.board(25)micro.board(9)print(sajha.riders, micro.riders)

Output

 

Three things to notice:

  • The object exists before it has a name. During __init__, only self points to it; sajha points to it once Bus(…) has finished.
  • During sajha.board(25) self points to sajha’s object, and during micro.board(9) to micro’s. The same line, self.riders = self.riders + n, changes a different bus each time.
  • A variable never holds an object inside it. It holds an arrow to the object, a reference. You will see why this matters later in the unit.

Figure 1.2 shows the class and its two objects as programmers draw them.

the class: one designBusplateseatsriders__init__(plate, seats)board(n)alight(n)__str__()sajha : Busplate = 'Ba 2 Kha 4512'seats = 40riders = 25micro : Busplate = 'Ba 3 Ja 118'seats = 14riders = 9made from Busplateseatsriders__init__(plate, seats)board(n)alight(n)__str__()two objects made from it:sajha : Busplate = 'Ba 2 Kha 4512'seats = 40riders = 25micro : Busplate = 'Ba 3 Ja 118'seats = 14riders = 9
Figure 1.2 The class Bus as a class diagram: its name, its attributes, its methods. With it, two objects made from it, each with its own values. One design, as many objects as you need.

Printing an object: __str__

Without the last method, print(sajha) would print something like <__main__.Bus object at 0x7f2599b6c2f0>: the object’s class and where it happens to be in memory, a different number on every run. __str__ is another special method: when Python needs an object as text (for print, or str(sajha)), it calls the object’s __str__ and uses the string it returns. Methods whose names start and end with two underscores are Python’s own hooks into your class: Python calls them for you at the right moment, so you seldom call them by name yourself.

Mistakes everyone makes with classes

Try it

Mistakes with classes

Each program stops with an error. Find the line Python names, the kind of error, and the fix.

What Python says

Traceback (most recent call last):
  File "main.py", line 11, in <module>
    sajha.board(25)
    ~~~~~~~~~~~^^^^
  File "main.py", line 8, in board
    self.riders = self.riders + n
                  ^^^^^^^^^^^
AttributeError: 'Bus' object has no attribute 'riders'

1. Which line does the message name? Tap it in the program.

Read the message from the bottom up: the last line says what went wrong, the line above it where.

Program 1 of 3 · right the first time: 0 of 9

a = Bus("Ba 2 Kha 4512", 40) and b = Bus("Ba 2 Kha 4512", 40). Then a.board(10). What is b.riders?

Pick an answer to see why.

Inside a method, what does self refer to?

Pick an answer to see why.

Drawing a class diagram

A program that models a real thing starts on paper, not at the keyboard. A good way to find the classes is to describe the system in a few plain sentences, then read them:

  • the nouns for things there are many of, each with facts of its own, are likely classes (a member, an account, a loan);
  • the facts about each are attributes (a member’s name, an account’s balance);
  • the verbs, what the things do or what is done to them, are likely methods (deposit, withdraw, add interest).

Then draw each class as a box in three parts, as Figure 1.2 did: the name at the top, the attributes in the middle, the methods at the bottom. This drawing is a class diagram. Its symbols come from UML (the Unified Modeling Language), a standard way of drawing software that programmers everywhere can read.

Try it on a savings and credit cooperative, the kind found in almost every town in Nepal: “Members save money in accounts. A member may have several accounts. Members deposit money, and withdraw it if there is enough. Members may borrow money as a loan, which grows by a month’s interest and is paid back in instalments.”

Try it

Class, attribute or method?

Decide what each part of the cooperative should be in the program.

a member

one of the people who own the cooperative

In a program for a savings cooperative, what should this be?

Tap a group above.

Card
1 of 12
Right the first time
0

Keeping an object’s data right

Back to the problem with the dictionary: any line of the program could set the riders to −3. With a class, there is a better way. If the rest of the program changes an object only by calling its methods, the methods can check every change, and the object’s data can never be wrong.

Here is a cooperative’s savings account. A balance must never go below zero, and a deposit must be more than nothing.

class Account:    """A member's savings account at a cooperative."""     def __init__(self, owner, opening):        self.owner = owner        self._balance = opening     def deposit(self, amount):        if amount <= 0:            raise ValueError("a deposit must be more than 0")        self._balance = self._balance + amount     def withdraw(self, amount):        if amount > self._balance:            raise ValueError("not enough savings")        self._balance = self._balance - amount     def balance(self):        return self._balance  sita = Account("Sita Magar", 500)sita.deposit(1200)sita.withdraw(300)print(sita.owner, "has Rs", sita.balance())try:    sita.withdraw(5000)except ValueError as e:    print("Refused:", e)print(sita.owner, "has Rs", sita.balance())

Output

Sita Magar has Rs 1400Refused: not enough savingsSita Magar has Rs 1400

Three things to notice here.

  • raise ValueError("…") stops the method with an error of your own, the kind you caught with try and except in Grade 10. The withdrawal that would have made the balance negative never happens, and the program that asked for it is told why. (except ValueError as e keeps the error in e, so it can print the message.)
  • _balance starts with an underscore. Python programmers use a leading underscore to say: this attribute is the object’s own business; don’t use it from outside the class, use the methods. Python doesn’t enforce it (sita._balance = -3 would still work), so it is an agreement between programmers. A saying among Python programmers is that “we are all consenting adults here”: the underscore is a clear sign, and anyone who ignores it is responsible for what breaks.
  • owner has no underscore. It is public: part of what the class offers, for any code to read. So the underscore divides an object’s names into two kinds: public names for everyone (owner, deposit()) and names for the class’s own use (_balance). A method can be marked the same way, such as _check(amount), for a helper only the class’s other methods call. Languages such as Java and C# write the two kinds as the words public and private, and there the language enforces it: code outside the class can’t use a private attribute at all.

Keeping an object’s data behind its methods, so that the rest of the program uses only the methods, is called encapsulation. It is Grade 10’s information hiding, for a single object: what the object offers (its methods, with their names, parameters and what they return) is its interface, and how it keeps its data is hidden inside.

Changing the inside, keeping the outside

The real reward of encapsulation comes later. Suppose the cooperative now wants every account to keep a passbook: a record of every deposit and withdrawal, not just the balance. Here is the class rewritten inside. The balance is no longer kept at all; it is worked out from the passbook. The program at the bottom is exactly the same as before, plus one line at the end.

class Account:    """A member's savings account at a cooperative, kept as a passbook."""     def __init__(self, owner, opening):        self.owner = owner        self._passbook = [("opening", opening)]     def deposit(self, amount):        if amount <= 0:            raise ValueError("a deposit must be more than 0")        self._passbook.append(("deposit", amount))     def withdraw(self, amount):        if amount > self.balance():            raise ValueError("not enough savings")        self._passbook.append(("withdrawal", -amount))     def balance(self):        return sum([amount for kind, amount in self._passbook])     def statement(self):        for kind, amount in self._passbook:            print(f"{kind:>10} {amount:>6}")  sita = Account("Sita Magar", 500)sita.deposit(1200)sita.withdraw(300)print(sita.owner, "has Rs", sita.balance())try:    sita.withdraw(5000)except ValueError as e:    print("Refused:", e)print(sita.owner, "has Rs", sita.balance())sita.statement()

Output

Sita Magar has Rs 1400Refused: not enough savingsSita Magar has Rs 1400   opening    500   deposit   1200withdrawal   -300

The answers are the same, and the code that uses the account didn’t change a single character. If the cooperative’s program had a hundred places that used accounts, none of them would need to change, because they only ever called deposit(), withdraw() and balance(). Had any of them read sita._balance directly, it would now fail, because there is no _balance any more. That is why the underscore matters.

Why does the Account class have a balance() method, instead of letting the program read an attribute directly?

Pick an answer to see why.

Objects that hold other objects

A bus carries passengers. A route is made of stops. A library holds books, and a cooperative holds its members. Real things are made of, or hold, other things, and objects can too: an attribute’s value can be another object, or a list of objects. An object that holds other objects is built by composition.

In the simulation later in this unit, four classes work together, as Figure 1.3 shows.

stops*waiting*riders*usesRoutestopsStopnamewaitingBusseatsridersarrive(stop)Passengergoing_to stops*waiting*riders*usesRoutestopsStopnamewaitingBusseatsridersarrive(stop)Passengergoing_to
Figure 1.3 The bus route simulation’s classes. A filled diamond at one end of a line means the class at that end holds the other (composition); * means “any number of them”. A Route holds its Stops, in its attribute stops; a Stop holds the Passengers waiting there; a Bus holds the Passengers riding it. The dashed arrow means “uses”: a Bus’s arrive() method is given a Stop. (Each class’s __init__ is left out, as class diagrams often do.)

Passing objects around

When a function is given an object, it gets a reference to the very same object, never a copy, just as a list passed to a function in Grade 10 was the same list. So a function can change an object it was given, and a list can hold objects that are also held somewhere else. This program has a function, get_on(), that makes a passenger and puts them on a bus it was given. Predict what the last line prints before you step through it.

Try it

Passengers on a micro

Press Next line. Watch how many arrows end at each object: a bus’s list, a function’s parameter and a second name can all point to the same passenger.

The program’s names

 
 
 
 

Objects

Step 1 / 23

Line 1: class Passenger:

class Passenger: makes the class Passenger, kept under its name. No passenger exists yet.

class Passenger:    def __init__(self, name, stop):        self.name = name        self.stop = stop class Bus:    def __init__(self, seats):        self.seats = seats        self.riders = []     def board(self, person):        if len(self.riders) < self.seats:            self.riders.append(person) def get_on(bus, name, stop):    p = Passenger(name, stop)    bus.board(p) micro = Bus(14)get_on(micro, "Asha", "Ratnapark")get_on(micro, "Dorje", "Kalanki")first = micro.riders[0]first.stop = "Thapathali"print(micro.riders[0].stop)

Output

 

Look at the end. first and item 0 of the bus’s list point to the same Passenger, so changing first.stop changed the passenger the bus is carrying. There is only one Asha. Two names for one object are called aliases, and they are useful (the bus and the stop can both know about the same passenger) as long as you expect them.

Notice also what happened to p, the passenger’s name inside get_on(): when the call ended, the name went with its box, but the passenger stayed, because the bus’s list still pointed to it. An object lasts as long as something points to it. When nothing points to an object any more, Python throws it away and reuses its memory; this is called garbage collection.

first = micro.riders[0] and then first.stop = "Thapathali". What happens to micro.riders[0].stop?

Pick an answer to see why.

A kind of: inheritance

Bagh-Chal, Nepal’s own board game, has two kinds of piece. Four tigers start at the corners of a board of 5 × 5 points; the goat player places twenty goats, one at a time, and once all are placed, moves them. Both kinds of piece stand on a point and move one step along a line to an empty point. But a tiger can also jump over a goat in a straight line to the empty point beyond, and capture it. The tigers win by capturing five goats; the goats win by trapping every tiger so it can’t move.

A wooden Bagh-Chal board with lines across it and diagonals, in a glass case, with wooden goat and tiger pieces standing on its points
Figure 1.4 A wooden Bagh-Chal set. The pieces stand where the lines cross, and move along the lines; from every second point, the diagonals go too. Photo: E.dronism, CC0

Goats and tigers are both pieces. They share almost everything (a position, the ways out of their point along the lines, an ordinary step) and differ in one thing, the tiger’s jump. Writing two classes that repeat the same code would mean every fix had to be made twice. Instead, a class can be made from another: it gets everything the other has, and adds or changes only what is different. This is inheritance. The class it is made from is the parent class (or superclass, or base class); the new one is the child class (or subclass).

class Piece:    """A piece on the Bagh-Chal board, at a point (row, col)."""    symbol = "?"     def __init__(self, row, col):        self.row = row        self.col = col     def directions(self):        """The ways out of this point along the board's lines."""        steps = [(-1, 0), (1, 0), (0, -1), (0, 1)]        if (self.row + self.col) % 2 == 0:      # a point with diagonal lines            steps = steps + [(-1, -1), (-1, 1), (1, -1), (1, 1)]        return steps     def moves(self, board):        """The empty points one step away along a line."""        found = []        for dr, dc in self.directions():            r, c = self.row + dr, self.col + dc            if board.on_board(r, c) and board.at(r, c) is None:                found.append((r, c))        return found  class Goat(Piece):    symbol = "G"  class Tiger(Piece):    symbol = "T"     def moves(self, board):        """A tiger's steps, and its jumps over a goat to an empty point."""        found = super().moves(board)        for dr, dc in self.directions():            r, c = self.row + dr, self.col + dc     # the point next to it            r2, c2 = r + dr, c + dc                  # the point beyond that            if board.on_board(r2, c2) and isinstance(board.at(r, c), Goat) and board.at(r2, c2) is None:                found.append((r2, c2))        return found  class Board:    """The 25 points of a Bagh-Chal board, and the pieces on them."""     def __init__(self):        self.pieces = []     def place(self, piece):        self.pieces.append(piece)     def on_board(self, row, col):        return 0 <= row < 5 and 0 <= col < 5     def at(self, row, col):        """The piece at a point, or None if it is empty."""        for p in self.pieces:            if p.row == row and p.col == col:                return p        return None     def show(self):        for row in range(5):            marks = []            for col in range(5):                p = self.at(row, col)                marks.append(p.symbol if p else "+")            print(" ".join(marks))  board = Board()tiger = Tiger(0, 0)board.place(tiger)for row, col in [(0, 1), (1, 0), (1, 1), (2, 2)]:    board.place(Goat(row, col))board.show()print("The tiger can go to:", tiger.moves(board))print("The goat at (1, 1) can go to:", board.at(1, 1).moves(board))

Output

T G + + +G G + + ++ + G + ++ + + + ++ + + + +The tiger can go to: [(2, 0), (0, 2)]The goat at (1, 1) can go to: [(2, 1), (1, 2), (0, 2), (2, 0)]

Read it slowly; each new piece of Python is small.

  • class Goat(Piece): makes Goat a child of Piece. The brackets name the parent. Goat’s body has only one line, so a Goat has everything a Piece has (__init__, directions(), moves()), with its own symbol.
  • symbol = "?", written in the class outside any method, is a class attribute: one value shared by every object of the class (a string, which never changes in place, so the list trap can’t happen). Goat and Tiger each set their own, so p.symbol is “G” for a goat and “T” for a tiger: Python looks on the object, then in its class, then in the parent.
  • Tiger’s moves() has the same name as Piece’s. A child’s method with the same name as its parent’s replaces it for the child’s objects. This is overriding. Python looks for a method first in the object’s own class, then in its parent: a goat finds moves() in Piece; a tiger finds its own.
  • super().moves(board) calls the parent’s version from inside the child’s. The tiger’s moves are the ordinary steps (worked out by Piece’s code, not copied) plus the jumps.
  • isinstance(board.at(r, c), Goat) asks whether the piece is a Goat (or a child of Goat). A tiger can jump a goat but not another tiger.
  • (self.row + self.col) % 2 == 0: look at a Bagh-Chal board, and you will see diagonal lines only through every second point, those where the row and column add up to an even number.

The tiger at the corner is hemmed in by three goats, so it has no ordinary step. But it can jump the goat below it to (2, 0) and the goat beside it to (0, 2). It can’t jump the goat at (1, 1), because the point beyond, (2, 2), is taken.

Figure 1.5 shows the program’s classes. The hollow triangle is UML’s sign for inheritance; notice that the diagram has both kinds of relation, holding and being a kind of.

pieces*is a kind ofBoardpiecesplace(piece)on_board(row, col)at(row, col)show()Piecesymbolrowcoldirections()moves(board)Goatsymbol = "G"Tigersymbol = "T"moves(board) pieces*is a kind ofBoardpiecesplace(piece)on_board(row, col)at(row, col)show()Piecesymbolrowcoldirections()moves(board)Goatsymbol = "G"Tigersymbol = "T"moves(board)
Figure 1.5 The Bagh-Chal program’s classes. A Board holds any number of Pieces (the filled diamond). Goat and Tiger are each a kind of Piece (the hollow triangle, at the parent’s end): they inherit Piece’s attributes and methods. Goat only sets its symbol; Tiger also has its own moves(board), which overrides Piece’s.

A promise every child keeps: abstract classes

Piece is a parent you could still make objects from: Piece(2, 2) would be a piece of no kind, with the symbol “?”. Sometimes a parent should never be made at all, only its children. A cooperative offers several kinds of account: a savings account earns interest every month at one rate, a fixed deposit at a higher one. Every kind must be able to say what interest it earns, but each works it out its own way, and there is no such thing as an account of no kind.

A class like that is an abstract class: a parent that only describes what its children must do. It lists the methods every child must have, its abstract methods, without writing them. Python’s abc module (for “abstract base classes”) makes one:

from abc import ABC, abstractmethod  class Account(ABC):    """Any account at the cooperative. Every kind must say what interest it earns."""     def __init__(self, owner, balance):        self.owner = owner        self._balance = balance     @abstractmethod    def month_interest(self):        """The interest this account earns in a month."""     def add_interest(self):        self._balance = self._balance + self.month_interest()        return self._balance  class Savings(Account):    rate = 0.06                         # a year, for every savings account     def month_interest(self):        return self._balance * Savings.rate / 12  class FixedDeposit(Account):    rate = 0.09     def month_intrest(self):            # a slip of the keyboard        return self._balance * FixedDeposit.rate / 12  sita = Savings("Sita Magar", 12000)print(sita.owner, "now has Rs", sita.add_interest())ram = FixedDeposit("Ram Thapa", 100000)print(ram.owner, "now has Rs", ram.add_interest())

It prints Sita Magar now has Rs 12060.0, and then stops at line 36:

Error message

Traceback (most recent call last):  File "main.py", line 36, in <module>    ram = FixedDeposit("Ram Thapa", 100000)TypeError: Can't instantiate abstract class FixedDeposit without an implementation for abstract method 'month_interest'
  • class Account(ABC): makes Account a child of ABC, which lets it have abstract methods.
  • @abstractmethod, on the line above def month_interest(self):, marks that method as a promise: every child must write its own month_interest(). The abstract method has no code of its own, only its docstring.
  • add_interest() is an ordinary method, written once in Account for every kind of account. It calls self.month_interest() without knowing which kind of account self is: a savings account’s own method runs for Sita.
  • The error. FixedDeposit misspelled the method’s name, so it never kept the promise, and Python refuses to make a FixedDeposit at all. An abstract class can’t be made either: Account("Hari", 500) stops with the same kind of error. The rates are numbers for the example, not any cooperative’s.

Notice when the mistake was found: the moment someone tried to make the broken object, not a month later, when the interest should have been added and a member’s passbook came out wrong. That is what an abstract class is for: a promise the language checks, so every child that forgets it is caught at once. Java and C# have the same idea: an abstract class, and an interface, a list of methods with no code at all.

Holding, or being a kind of?

Inheritance is easy to overuse. Before making one class a child of another, say the sentence aloud.

  • “A tiger is a kind of piece.” True, and everything a piece can do, a tiger can do. Inheritance fits.
  • “A bus is a kind of passenger list.” False. A bus has a list of passengers. That is composition: give the bus an attribute that holds the list.
  • “A bus is a kind of engine.” False again; it has an engine. If Bus inherited from Engine, every bus would have the engine’s methods (fuel_pump(), oil_level()) as its own, and changing to an electric motor would mean changing what a bus is.

The test is “is a kind of” (inherit) against “has a” (hold). When in doubt, hold: composition joins classes more loosely, so each can change without the other. Many experienced programmers keep inheritance for a few clear families, like pieces of a game, and build everything else by composition.

Which pair is best joined by inheritance (the first a child class of the second)?

Pick an answer to see why.

class Tiger(Piece) defines its own moves(). A Goat object calls moves(). Which code runs?

Pick an answer to see why.

A simulation built from objects

Objects are especially good at simulation: a program that imitates a real system, so you can try out questions you can’t easily try in the world. How many people are left behind at each stop if Sajha runs a micro instead of a big bus on a busy route? Here is a simple model, built from the four classes of Figure 1.3. People wait at each stop to go to some stop further along; the bus arrives at each stop in turn, lets off the people going there, then takes on people who are waiting, as long as there are seats.

import random  class Passenger:    def __init__(self, going_to):        self.going_to = going_to          # the name of the stop to get off at  class Stop:    def __init__(self, name):        self.name = name        self.waiting = []                 # passengers waiting here, first come first  class Route:    def __init__(self, names):        self.stops = [Stop(name) for name in names]  class Bus:    def __init__(self, seats):        self.seats = seats        self.riders = []     def arrive(self, stop):        """People get off, then people get on while there are seats.        Returns how many got off and how many got on."""        staying = [p for p in self.riders if p.going_to != stop.name]        off = len(self.riders) - len(staying)        self.riders = staying        on = 0        while stop.waiting and len(self.riders) < self.seats:            self.riders.append(stop.waiting.pop(0))            on = on + 1        return off, on  random.seed(7)route = Route(["Lagankhel", "Jawalakhel", "Pulchowk", "Kupondole",               "Thapathali", "Maitighar", "Ratnapark"])for i in range(len(route.stops) - 1):    later = route.stops[i + 1:]    for k in range(random.randint(4, 12)):        route.stops[i].waiting.append(Passenger(random.choice(later).name)) bus = Bus(14)for stop in route.stops:    off, on = bus.arrive(stop)    print(f"{stop.name:<10} off {off:>2}  on {on:>2}  aboard {len(bus.riders):>2}  left behind {len(stop.waiting)}")

Output

Lagankhel  off  0  on  9  aboard  9  left behind 0Jawalakhel off  3  on  4  aboard 10  left behind 0Pulchowk   off  3  on  7  aboard 14  left behind 3Kupondole  off  6  on  4  aboard 12  left behind 0Thapathali off  5  on  7  aboard 14  left behind 1Maitighar  off  7  on  5  aboard 12  left behind 0Ratnapark  off 12  on  0  aboard  0  left behind 0

Each object looks after itself. A Stop keeps its own queue of waiting people; the Bus decides who gets off and who gets on; the program at the bottom only sets things up and sends the bus along the route. random.seed(7) makes the “random” passengers the same on every run (Grade 9), so you can compare one change at a time.

The micro filled up twice, at Pulchowk and at Thapathali, and four people were left behind. Change Bus(14) to Bus(40) and run it again: nobody is left behind, and the bus never has more than 17 people on board. Those four people are still there in the program, too: they are in stop.waiting, because the Stop objects remember them.

An older man with white hair and glasses, in a checked shirt
Figure 1.6 Kristen Nygaard in 1997. Photo: Jorge Stolfi, Public domain

Objects or not?

Programming with classes and objects is called object-oriented programming, often shortened to OOP. It is not the only good way to write programs, and a skilled programmer uses it where it helps and leaves it out where it doesn’t. Compare the two ways you have seen.

Dictionaries and functions Classes and objects
Where the code for a thing is spread among the program’s functions in one place: the class
A misspelled name, such as rider a new key, made quietly an AttributeError when it is read or called (set from outside, it is still made quietly)
Keeping the data correct every function must remember the rules the methods check every change
Changing how the data is kept every function that reads it changes only the class changes, if the program uses its methods
Many kinds of a thing, each a little different if/elif on a “kind” field, in every function that cares a child class for each kind, each with its own methods
A small program, or one calculation short and clear more to write and to read

The fifth row is worth seeing in code. Without classes, the Bagh-Chal pieces’ moves would need a test of the kind of piece:

def moves(piece, board):    found = steps(piece, board)    if piece["kind"] == "tiger":        found = found + jumps(piece, board)    return found

That is fine for two kinds. But if you invented a third kind of piece (say, one that may only move diagonally), every function with an if on the kind must be found and changed. With classes, you add one child class with its own moves(), and every line that says piece.moves(board) works for it unchanged. The same call doing the right thing for each kind of object is called polymorphism (from Greek: “many shapes”).

But not everything needs a class. Grade 10’s grade_for(percent) is a pure function: it keeps no data between calls and has nothing to protect. Wrapping it in a class would only add lines. A good rule of thumb: when you find several functions that all take the same dictionary and change it, that dictionary and those functions want to be a class.

A woman with red hair and glasses, smiling, speaking at a lectern with a laptop
Figure 1.7 Adele Goldberg in 2007. She was also president of the ACM, the world’s largest society of computing professionals, from 1984 to 1986. Photo: Terry Hancock, CC BY-SA 2.5

Which program would gain least from being rewritten with classes?

Pick an answer to see why.

Project: a program built from objects

Choose one, and build it from classes of your own.

  • A bus route, more real. Start from the simulation. Add new people arriving at each stop every few minutes, several buses on the route one after another, and a fare (Rs 20 for up to five stops, Rs 25 for more) that each bus adds up. Then ask a question with it: on your route, is it better to run a big bus every 20 minutes or a micro every 10?
  • Bagh-Chal for two players. Finish the game: the goats’ placing phase (twenty goats, placed one per turn), moves typed as 2 3 1 3 (from row 2, column 3 to row 1, column 3), captures, and both ways of winning. Keep the rules in the classes: a Board, Piece, Goat and Tiger, and a Game that knows whose turn it is.
  • A cooperative. Members, Accounts and Loans from the example’s class diagram, with interest added each month, a statement for each member, and a report of the cooperative’s total savings and loans.
  1. Model it on paper first. Write the system in a few sentences; underline the nouns and the verbs; draw the class diagram, with the composition and any inheritance marked. Say each relation aloud: “is a kind of” or “has a”?
  2. One class at a time. Write a class, make two or three objects from it, and test each method before you write the next class. Print objects with __str__.
  3. Protect the data. Decide the rules each class must keep (no negative balance, no two pieces on one point), check them in the methods, and use underscores for attributes that are the object’s own business.
  4. Build the program that makes the objects and sends them messages. Keep it short: the work belongs in the methods.
  5. Change something inside. When it works, change how one class keeps its data (a passbook instead of a balance; the board as a dictionary of points instead of a list of pieces). If you protected the data in step 3, only that class should change.
  6. Show and explain. Show your class diagram beside the running program, and explain one place where you chose composition and why.

You can write and run each class on this page’s programs, even on a phone: edit a program and press Run. A program of several hundred lines is easier on a computer, in a Python editor (Grade 10).

In short

Exercise

What is the difference between a class and an object?

Pick an answer to see why.

A class has def __init__(self, name, roll):. Which line makes an object of it correctly?

Pick an answer to see why.

Inside __init__, a student writes marks = [] instead of self.marks = []. What happens when a method later uses self.marks?

Pick an answer to see why.

What does super().moves(board) do inside Tiger.moves?

Pick an answer to see why.

Which is an example of encapsulation?

Pick an answer to see why.

Which of these relations is “has a”, so best built by composition?

Pick an answer to see why.

Every savings account at a cooperative earns the same interest rate, so the class has rate = 0.06 written in it, outside every method. The cooperative raises the rate for every account to 7%. Which line does that?

Pick an answer to see why.

In a class Member, one attribute is called phone and another _pin. What do the names tell another programmer?

Pick an answer to see why.

a.

Explain these terms, with an example of each from the unit: class, object, attribute, method, constructor.

Answer
b.

What is self? Explain what happens, step by step, when Python runs micro.board(18), using the Bus class of this unit (14 seats, nobody on board yet).

Answer
c.

Write a class Student with attributes name and marks (a list, empty at first), and methods add_mark(mark), which adds a mark only if it is between 0 and 100, and average(), which returns the average of the marks, or 0 if there are none. Make one student, add the marks 72, 105 and 64, and print the average.

Answer
d.

A cooperative’s program keeps each account as a dictionary, {"owner": "Sita", "balance": 500}, and several functions change account["balance"] directly. List three problems this can cause, and explain how an Account class with methods solves each.

Answer
e.

What does this program print? Explain why, using the idea of references.

class Box:    def __init__(self, n):        self.n = n def double(box):    box.n = box.n * 2 a = Box(5)b = adouble(b)c = Box(5)print(a.n, b.n, c.n)
Answer
f.

Draw a class diagram for a school library: books, each with a title, an author and whether it is on loan; members, each with a name and the books they have borrowed (at most 3); and the library, which holds its books and members. Give each class at least one method, and mark which class holds which.

Answer
g.

A school’s program has a class Person with attributes name and phone, and a method contact_card(). Write two child classes, Student (with a grade) and Teacher (with a subject), each with an __init__ that uses super().__init__ for the name and phone, and with its own contact_card() that adds its extra fact. Make one of each and print their cards.

Answer
h.

For each pair, say whether the first should inherit from the second or hold it, and give the sentence that decides it: (i) Car and Engine; (ii) SavingsAccount and Account; (iii) Bus and Route; (iv) Tiger and Piece; (v) Playlist and Song.

Answer
i.

In the bus route simulation, what would change if the line self.riders = staying in arrive() were left out? Predict, then explain using what the program’s objects hold.

Answer
j.

Kanchan writes a class for each kind of vehicle in a traffic program, Bus, Micro, Truck and Motorbike, each with its own copy of the same move(), stop() and position code, and different max_speed and seats. Suggest a better design. Draw its class diagram, and say what you gained.

Answer
k.

Rewrite this program with a class Shop, so that the stock can never go below zero and a sale of something the shop doesn’t sell is refused with an error message. Keep the output the same for the two good sales.

stock = {"momo": 40, "chowmein": 25} def sell(stock, item, plates):    stock[item] = stock[item] - plates    print("Sold", plates, item) sell(stock, "momo", 12)sell(stock, "chowmein", 30)sell(stock, "pizza", 1)
Answer
l.

“Object-oriented programming is always better than writing functions.” Do you agree? Give two situations where classes make a program clearer and easier to change, and two where plain functions are better, with a reason for each.

Answer
m.

A school garden has round and rectangular flower beds. Write an abstract class Shape with one abstract method, area(), and two child classes, Circle (with a radius) and Rectangle (with a width and a length). Make a list of three beds, a circle of radius 3 m, a rectangle of 4 m by 6.5 m and a circle of radius 1.2 m, and print each bed’s area and the total. What would happen if someone added a Triangle class and forgot to write its area()?

Answer