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.
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,seatsandriders, 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()andalight().
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 MOMOThe 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 takenTake 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 tellBus(the design) frombus(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 isself, 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 asselfand your values asplateandseats; then it gives you the object, andsajhapoints to it.self.plate = plategives the object an attribute.platealone is__init__’s parameter, gone when__init__ends;self.plateis 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.
Three things to notice:
- The object exists before it has a name. During
__init__, onlyselfpoints to it;sajhapoints to it onceBus(…)has finished. - During
sajha.board(25)self points to sajha’s object, and duringmicro.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.
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
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.”
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 1400Three things to notice here.
raise ValueError("…")stops the method with an error of your own, the kind you caught withtryandexceptin 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 ekeeps the error ine, so it can print the message.)_balancestarts 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 = -3would 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.ownerhas 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 wordspublicandprivate, 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 -300The 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.
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.
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.
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, sop.symbolis “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 findsmoves()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.
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 ofABC, which lets it have abstract methods.@abstractmethod, on the line abovedef month_interest(self):, marks that method as a promise: every child must write its ownmonth_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 callsself.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 0Each 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.
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 foundThat 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.
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.
- 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”?
- 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__. - 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.
- Build the program that makes the objects and sends them messages. Keep it short: the work belongs in the methods.
- 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.
- 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.
Explain these terms, with an example of each from the unit: class, object, attribute, method, constructor.
Answer
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
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
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
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
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
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
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
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
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
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
“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
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
Quiz yourself: Algorithms and programming, Grades 6–12 →
Practice every question in the book, and keep track of the ones you miss →
