Exam practice
Every question from the end of every unit, with hints and answers to open. After you check an answer, mark how you did, and come back to the ones you didn’t get.
13 questions
Unit 1 · Objects and Classes
Explain these terms, with an example of each from the unit: class, object, attribute, method, constructor.
Answer
- Class: the design that objects are made from; it says what attributes and methods every object of it has. Example:
class Bus:. - Object (instance): one thing made from a class, with its own values for the attributes. Example:
sajha = Bus("Ba 2 Kha 4512", 40). - Attribute: a value an object keeps about itself. Example:
sajha.seats, 40. - Method: a function that belongs to a class and works on the object it is called on. Example:
sajha.board(25). - Constructor: the method that sets up a new object’s attributes when it is made; in Python,
__init__. Example: Bus’s__init__(self, plate, seats)gives each bus its plate, its seats and riders = 0.
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
self is the method’s first parameter, which receives the object the method was called on.
- Python finds
boardin micro’s class, Bus, and calls it asBus.board(micro, 18): self is micro’s object and n is 18. room = self.seats - self.ridersreads micro’s own attributes: 14 − 0 = 14.got_on = min(n, room): min(18, 14) = 14, because only 14 seats are free.self.riders = self.riders + got_onchanges micro’s riders to 14; no other bus changes.- The method returns 14, the number who got on. (4 people are left at the stop.)
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
class Student: def __init__(self, name): self.name = name self.marks = [] def add_mark(self, mark): """Adds a mark, if it is a possible one (0 to 100).""" if 0 <= mark <= 100: self.marks.append(mark) def average(self): """The average of the marks, or 0 if there are none.""" if not self.marks: return 0 return sum(self.marks) / len(self.marks) mina = Student("Mina")for m in [72, 105, 64]: mina.add_mark(m)print(mina.name, mina.average())Output
Mina 68.0105 is refused, so the average is of 72 and 64: 136 ÷ 2 = 68.0. The list is made in __init__ with self.marks = [], so each student has their own (not a list written in the class, which every student would share).
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
- Wrong values: any line can set the balance to a negative number or forget a rule. In a class, only
deposit()andwithdraw()change the balance, and they refuse what the rules don’t allow, so the balance is always right. - Misspellings:
account["balnce"] = 700silently makes a new key, and the real balance doesn’t change. With a class, the rest of the program only calls methods, and a misspelled method,sita.depsit(700), is an AttributeError, so the mistake is found at once. (Setting a misspelled attribute from outside,sita.balnce = 700, would still make a new attribute quietly: one more reason to change an object only through its methods.) - Changing how data is kept: if the cooperative wants a passbook of every deposit and withdrawal, every function that touches
"balance"must be found and changed. With a class, only the class changes; code that callsbalance()anddeposit()carries on as before.
(Also good: the code for accounts is in one place, the class, instead of spread through the program.)
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
Output
10 10 5b = a makes a second reference to the same Box, not a copy. double(b) gives the function a reference to that same Box, and box.n = box.n * 2 changes it to 10, so both a.n and b.n are 10. c = Box(5) makes a separate object, so c.n is still 5.
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
| Library | Book | Member |
|---|---|---|
| books, members | title, author, on_loan | name, borrowed |
| add_book(book), find(title), lend(book, member), take_back(book, member) | __str__() |
can_borrow() |
- Library holds any number of Books (in
books) and any number of Members (inmembers): composition, a diamond at the Library’s end and * at the other. - A Member holds up to 3 Books in
borrowed(a diamond at the Member’s end, “0..3” at the Book’s): the same Book objects the library holds, not copies. lend()checks the rules: the book isn’t on loan, andmember.can_borrow()(fewer than 3 borrowed). Then it setsbook.on_loan = Trueand adds the book to the member’s list.
There is no inheritance here: none of these is a kind of another. Other good designs exist (a Loan class with a due date, for example).
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
class Person: def __init__(self, name, phone): self.name = name self.phone = phone def contact_card(self): return f"{self.name}, {self.phone}" class Student(Person): def __init__(self, name, phone, grade): super().__init__(name, phone) self.grade = grade def contact_card(self): return super().contact_card() + f", Grade {self.grade}" class Teacher(Person): def __init__(self, name, phone, subject): super().__init__(name, phone) self.subject = subject def contact_card(self): return super().contact_card() + f", teaches {self.subject}" people = [Student("Pemba Sherpa", "9841000001", 11), Teacher("Ms. Karki", "9851000002", "computing")]for p in people: print(p.contact_card())Output
Pemba Sherpa, 9841000001, Grade 11Ms. Karki, 9851000002, teaches computingsuper().__init__(name, phone) lets Person set the shared attributes, so that code is written once. Each child overrides contact_card() and uses Person’s version for the shared part. The loop calls p.contact_card() without asking what kind of person p is: polymorphism. (The phone numbers are made up.)
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
- Car and Engine: hold. “A car has an engine”; a car is not a kind of engine.
- SavingsAccount and Account: inherit. “A savings account is a kind of account”: it does everything an account does, perhaps with interest added.
- Bus and Route: hold (a reference to it). “A bus runs on a route”; it is not a kind of route, and many buses share one route object.
- Tiger and Piece: inherit. “A tiger is a kind of piece.”
- Playlist and Song: hold. “A playlist has songs”, a list of Song objects.
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
Nobody would ever get off. staying is a new list of the riders not getting off here, and off counts the difference, so the program would still print the right number “off”; but the bus’s own riders would keep everyone. The bus would fill up after a few stops, people would be left behind everywhere after that, and “aboard” would never go down. The printed numbers would contradict each other (people “off”, yet as many aboard), which is a good clue in debugging: when a count and a list disagree, look for the line that should have changed the object.
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
Make a parent class Vehicle with what every vehicle shares: attributes position, speed, max_speed, and methods move() and stop(). Make Bus, Micro, Truck and Motorbike children of Vehicle (each “is a kind of vehicle”), each setting its own max_speed and seats (a truck has a load instead), and overriding a method only where it really behaves differently (a bus has board() and alight(), which a truck doesn’t).
Diagram: Vehicle at the top (position, speed, max_speed / move(), stop()); four boxes below it, each joined to Vehicle by a line with a hollow triangle at Vehicle’s end; Bus and Micro also list seats, riders / board(n), alight(n); Truck lists load.
Gained: the shared code is written once, so a fix to move() is made in one place; a new kind of vehicle (an electric tempo) is one small child class; and code that moves all vehicles calls v.move() for any of them (polymorphism). Since Bus and Micro differ only in their numbers, they could even be one class, Bus, with different seats, which is simpler still.
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
class Shop: def __init__(self, stock): self._stock = dict(stock) # a copy, so the shop's stock is its own def sell(self, item, plates): if item not in self._stock: print("Refused: we don't sell", item) elif plates > self._stock[item]: print("Refused: only", self._stock[item], item, "left") else: self._stock[item] = self._stock[item] - plates print("Sold", plates, item) shop = Shop({"momo": 40, "chowmein": 25})shop.sell("momo", 12)shop.sell("chowmein", 30)shop.sell("pizza", 1)Output
Sold 12 momoRefused: only 25 chowmein leftRefused: we don't sell pizzaIn the original, the second sale would make the chowmein stock −5, and the third would stop the program with a KeyError. In the class, the only way to change the stock is sell(), which checks both rules. (dict(stock) copies the dictionary it is given, so a change to the caller’s dictionary later can’t change the shop’s stock behind its back.)
“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
Not always: it depends on the program.
Classes help:
- Many things, each with its own data and rules: buses, stops and passengers in a simulation; accounts in a cooperative. Each class keeps its data and the code that changes it together, and its methods protect its rules.
- Many kinds of a thing that behave a little differently: goats and tigers, or vehicles. Child classes override a method, and the program calls
piece.moves(board)without an if/elif on the kind, so a new kind is one new class.
Functions are better:
- A calculation with nothing to remember: converting °F to °C, or
grade_for(percent). A pure function is short and easy to test; a class would add lines and nothing else. - A short script that runs once: reading a file, totaling a column and printing the answer. There is no data that lives on or needs protecting, so a few functions are clearer.
A sign that a class is wanted: several functions that all take the same dictionary and change it.
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
import mathfrom abc import ABC, abstractmethod class Shape(ABC): """Any flat shape. Every kind must say its area.""" @abstractmethod def area(self): """The area, in square meters.""" class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return math.pi * self.radius ** 2 class Rectangle(Shape): def __init__(self, width, length): self.width = width self.length = length def area(self): return self.width * self.length beds = [Circle(3), Rectangle(4, 6.5), Circle(1.2)]for bed in beds: print(f"{bed.area():.1f} m²")print(f"In all: {sum(bed.area() for bed in beds):.1f} m²")Output
28.3 m²26.0 m²4.5 m²In all: 58.8 m²The loop calls bed.area() without asking what kind of bed it is: each object runs its own class’s area() (polymorphism), and the abstract class promises that every kind has one. A Triangle without area() couldn’t be made at all: Triangle(…) stops at once with TypeError: Can't instantiate abstract class Triangle without an implementation for abstract method 'area', before any wrong total could be printed.
