7 concepts19 worked examples25 exercises3 exam-level7 figures
What are you here for?
08 Classes and Object-Oriented Programming (Chapter 8)
Start with this
One question before you read anything. Getting it wrong is the point: it shows you what this section is for.
§08.0 — what a function with no return hands back
Three lines, and the middle one keeps the value of a call that was never asked to produce one.
defshorten(word, keep):
"""Assumes word is a str and keep an int > 0. Prints the first keep characters of word."""print(word[:keep])
short = shorten('catalogue', 4)
print(short)
print(type(short))
Find(a) Write the three lines this prints.
Given
shorten prints and has no return statement.
The call is shorten('catalogue', 4) and its value is kept in short.
IPython console
In [1]: %run untitled0.py
The function did its job on the screen and still handed back nothing. A behaves exactly this way, which is why b = obj.set_price(150) leaves None in b rather than the or the price.
Hint 1/4
Separate the two things the middle line does: it runs the function, and it keeps the value of the call.
Hint 2/4
A function that reaches its end with no return hands back None. Printing is not returning: it puts characters on the screen and the value of the call is still None.
Hint 3/4
Here shorten('catalogue', 4) prints the first four characters, and the value it hands back goes into short.
Hint 4/4
The first line is the four characters, then None, then the type of None.
Show solution
Read the function first and the call second. Reading the call first invites you to answer what appeared on the screen, which is a different question from what the call is worth.
Run the body
$$\texttt{print(word[:keep])}$$
Puts cata on the screen. This is the only visible effect of the call and it has nothing to do with the value of the call.
Ask what the call is worth
$$\texttt{short} = \texttt{None}$$
The body ends with no return, so the call is worth None.
$$\texttt{type(None)}$$
Printing None shows the word None, and printing its type shows the NoneType.
Independent check: put print(shorten('ab', 1)) in a program of your own.
What this looks like in Spyder
The tab next to Help. It lists every name your program left behind with its type and its size, so it answers the one question a listing cannot: what is in this name right now. The stepper on this page shows the same four columns.
Spyder 5.5.1 on Linux, running the program in its own editor. A different version moves a few toolbar icons; the panes are named the same.
A stock list is kept in three lists side by side: the names in one, the counts in another, the prices in a third. To report the smallest item the program sorts the counts and prints the first name. It reports a name that belongs to a different product, and every count and every price in that report is now attached to the wrong thing.
# Three parallel lists. One book is spread across three places.
titles = ['Ada', 'Zeno', 'Milo']
pages = [312, 98, 204]
prices = [180.0, 75.5, 120.0]
pages.sort()
print('shortest book:', titles[0], 'with', pages[0], 'pages')
Sample Run:
shortest book: Ada with98 pages
The 98 page book is Zeno. Nothing in the program is broken: the sort did exactly what it was asked to do, on one of the three lists.
By the end of this section you can take a written specification of the kind the lab sheets use, with data members, get and set methods, a and a sample run, and write the class that matches it. You can also take a short program that builds objects, a and sorts a list of them, and write down exactly what it prints.
In 60 seconds
A class is a type you write yourself: it says which slots every object of that type gets and which methods it answers to. Everything else in this section follows from two facts, that the object before the dot is handed to the method as self, and that Python looks for a method in the object's own class before it looks in the parent.
Every method definition and every method call. The definition always has one parameter more than the call has arguments, and that first parameter is self.
Any attribute the specification calls private. The short name only works in code written inside that class, which is what makes get and set methods the way in.
One class variable, one value, every object
$$\texttt{Book.\_\_fee}\ \text{is one slot}\quad\texttt{self.\_\_fee = v}\ \text{makes another}$$
A fee, a rate or a capacity that the specification says is the same for all objects.
print looks for __str__, containers look for __repr__
Any sample run that shows a block of lines for one object, or a list of objects with commas and brackets around them.
sort, min and in are calls to your own methods
$$\texttt{L.sort()}\to\texttt{\_\_lt\_\_}\quad\texttt{e in L}\to\texttt{\_\_eq\_\_}$$
A specification that says objects are ordered by one field with another as a tie breaker, or that two objects count as the same when certain fields match.
The search for a method starts at the object's own class
Every question that asks which version of a method runs. An override stops the search, and anything not written in the subclass is answered by the parent.
Three most common mistakes
Writing a method without self in its parameter list and then calling it on an object. The call passes the object anyway, so the program stops with a TypeError about the number of arguments.
Reaching a of the parent from inside a subclass, as self.__pages. Inside the subclass those two underscores are rewritten with the subclass name, so the attribute it asks for does not exist and the program stops with an AttributeError.
Raising a shared value with self.__fee = Book.__fee + 1. That line builds a private slot in one object and leaves the shared value untouched, so every other object still reads the old one and nothing in the output looks wrong until you check a second object.
Labs are 20 per cent of the course mark, the midterm 40 and the final 40. This week comes after the midterm, so the places this material is marked are the lab and the final. In the one autumn term recorded in the course material the lab mark was given in coarse steps, the lowest of ten labs was dropped and there was no makeup lab. One past final paper spent a whole question on writing a class together with the script that uses it, and one part of its tracing question on which inherited method a call reaches.
How much time do you have?
10 minutes
The shape every lab answer starts from: the class line, __init__ with one private slot per data member, and one per slot.
The 60-second card · A class is a type you write, and an object is one thing of that type · Hidden data, and get and set methods as the only way in · Formula card
45 minutes
Enough to write the class a lab sheet asks for, print it in the format of the sample run, and sort a list of objects, plus the traces that the final marks line by line.
The 60-second card · A class is a type you write, and an object is one thing of that type · A method is a function in the class, and its first parameter is the object · Hidden data, and get and set methods as the only way in · Giving the class a printed form with __repr__ · Special methods · Scaffolding comes off · B · computation
full read
Adds the three parts that cost marks rather than time: a class variable and what a write through self does to it, with the parent's init and printed form called from the child, and why a private attribute of the parent cannot be reached from inside the child.
The 60-second card · Recall first · Conventions · A class is a type you write, and an object is one thing of that type · A method is a function in the class, and its first parameter is the object · Hidden data, and get and set methods as the only way in · A class variable is one slot for the whole class · Giving the class a printed form with __repr__ · Special methods · Extending a class · Method boxes · Look-alike pairs · Scaffolding comes off · Full exam-style question · A · concept · B · computation · C · exam level · D · interleaved · Mistake ledger · Formula card · Check yourself
By the end of this section
Build a class with an __init__ method and create several objects from it, each with its own values in its own slots.
Write methods that use the data of the object they were called on, and say for each one whether it changes the object or hands a value back.
Hide the data of a class behind two underscores and reach it from outside only through get and set methods, with the condition the specification asks for inside the set method.
Distinguish a class variable from an and predict what two objects of the class share after a method has written to it.
Give a class a printed form with __repr__ and predict what a single object and a list of objects show on the screen.
Define the special methods that make the comparison operators, addition, sort, min and in work on objects of your own class.
Extend a class with a subclass, override a method, call the parent version from the child, and trace which version of a method a given call reaches.
Syllabus coverage
covered
Classes — covered
Writing a new type with the class keyword: the __init__ method, the self reference, instance variables, creating several objects of the same class, methods that use the object's own data, methods that take another object of the same class, and default values for parameters of __init__.
covered
Object-Oriented Programming — covered
The two principles the course names: , which is keeping the data and the methods that work on it together in one class, and , which is offering a public set of methods while the data behind them stays private.
covered
Chapter 8 — covered
Inheritance as the chapter presents it: a subclass that extends a parent, calling the parent init and the parent printed form from the child, overriding a method, the order in which Python searches for a method, isinstance, a subclass that adds nothing, and what happens when a subclass reaches for a private attribute of its parent.
deferred
Writing the search and sort algorithms themselves — deferred
Giving a class the __lt__ method is on this page, because a specification asks for it and because the built in sort calls it.
Deferred to the later sections on algorithmic complexity and on searching and sorting, which the syllabus lists after this week.
Recall first
A function, its parameters, and what it hands back
def area(w, h): names two parameters, and return w * h decides the value of the call. A function that reaches its end without a return hands back None, and so does a function whose return has nothing after it.
A method is a function written inside a class with one extra parameter at the front. Every rule about parameters, arguments and return values carries over unchanged, including the None, which is what a set method hands back.
A name holds a reference, not a copy
After b = a where a is a list, there is one list and two names for it. A change made through either name is visible through the other, and a is b is True.
An object of your own class behaves exactly like this. Two names for one object, one object in a list and a name for the same object, an object handed to a function: in all three cases there is one object, and a method that changes it changes it everywhere.
is against equals equals
a is b asks whether the two names are for one object. a == b asks whether the contents match, and for the types you have met so far the contents are what it compares.
For a class you write yourself, == starts out asking the same question as is, which is why two objects built from the same values are not equal until you say what equal means.
Reading a file into a list, line by line
in_file = open(name, 'r'), then for line in in_file:, then in_file.close(). Each line still carries its newline, so line.strip() first and line.strip().split(',') to break it into fields. Every field is a string, so a number needs int or float round it.
Every exercise this week ends the same way: a file of records becomes a list of objects, one object per line. The loop is the one you already have, and only the body changes, from appending a list to appending an object.
format, for fixed decimals and leading zeros
format(1620.5, '.2f') gives the text 1620.50. With positions, '{0:02d}:{1:02d}'.format(4, 7) gives 04:07, where the 0 before the 2 says to pad with zeros and the d says the value is a whole number.
A __repr__ has to return a string and has to match the sample run character for character. Both of these appear in the lab sample runs of this week, prices with two decimals and times with two digits.
The two families of list operation
L.append(e), L.sort() and L.remove(e) change the list and hand back None. sorted(L), L[a:b] and L + L2 leave it alone and hand back something new. L.pop(i) does both.
Your own methods fall into the same two families, and the specification decides which. Sorting a list of objects uses sort, so the list is changed in place and the call hands back None, and a set method is in the same family.
Try it yourself first (2 questions)
1§08.0 — one list, two names
A program saves the marks under a second name before it sorts them, so that the original order is still available afterwards.
marks = [70, 45, 90]
backup = marks
marks.append(55)
marks.sort()
print(marks)
print(backup)
print(marks is backup)
Find(a) Which of the four is what the program prints?
Given
marks starts as [70, 45, 90].
backup = marks is written before the list is changed.
Hint 1/4
Count the lists in this program before you count the changes. That number decides the whole answer.
Hint 2/4
An assignment of one name to another copies the reference, not the object.
Hint 3/4
Here the second line is backup = marks, which is an assignment between two names and not a slice, so 55 is appended to and the sort is applied to the one list both names are for.
Hint 4/4
Both printed lines are the sorted list, and is reports True.
Show solution
Answer the is line first even though it is printed last.
Count the objects
$$\texttt{backup = marks}$$
One list, two names. Nothing was built here, so there is nothing for the change to leave behind.
Also in place. The list both names are for is now sorted, so there is no second order left anywhere.
Answer $$\boxed{\texttt{[45, 55, 70, 90]}\ \text{twice, then True}}$$
Check
Independent check: the lengths agree with one object. len(marks) and len(backup) are both 4 at the end, and a real copy would have left one of them at 3.
2§08.0 — sort against sorted
Two ways of putting three words in order, one on each line, and then the list itself. The names were chosen to be unhelpful on purpose.
words = ['zeta', 'ada', 'milo']
first = sorted(words)
second = words.sort()
print(first)
print(second)
print(words)
Find(a) Write the three lines this prints.
Given
words starts as ['zeta', 'ada', 'milo'].
first keeps the value of sorted(words).
IPython console
In [1]: %run untitled0.py
The third line is the one worth reading twice. words is in order at the end, and not because of sorted, which never touched it, but because of sort, whose own value was None.
Hint 1/4
Ask two questions of each of the two middle lines, not one: what does it do to words, and what is it worth.
Hint 2/4
The function sorted builds a new list and leaves its argument alone. The method sort rearranges the list it is called on and hands back None.
Hint 3/4
Here sorted(words) is on the second line and words.sort() is on the third, so by the time the last print runs, both have happened.
Hint 4/4
The first line is the ordered list, the second is None, and the third is the ordered list again.
Show solution
Take the two calls in the order they appear rather than grouping them by what they are called.
The function that builds
$$\texttt{first} = \texttt{sorted(words)}$$
A new list in order. words is untouched at this moment, so its order is still zeta, ada, milo.
The method that rearranges
$$\texttt{second} = \texttt{words.sort()}$$
The list is put in order in place, and the value of the call is None, which is what lands in second.
$$\texttt{print(words)}$$
Reads the list after both lines, so it shows the order the sort left behind.
Independent check: first is words would be False and first == words would be True at the end.
Notation
symbol
reads as
means
watch out
$\texttt{class Book(object):}$
define a class called Book that extends object
Starts a new type. The indented lines below belong to it. The word in brackets is the class this one inherits from, and object is the class every other class comes from.
Writing class Book: with no brackets means the same thing in Python 3. The course writes the brackets and the lab sheets do too, so this page keeps them.
$\texttt{def \_\_init\_\_(self, title, pages):}$
the method that runs while a new object is being made
Two underscores on each side. It runs once, at the moment the class name is called, and its job is to put the starting values into the slots of the new object.
You never call it by name. Book('Ada', 312) calls it for you and hands you the object, which is why the call has one argument fewer than the definition has parameters.
$\texttt{self}$
this object, the one the method was called on
The first parameter of every method that uses the object's own data. Python fills it in from whatever stands before the dot in the call.
It is a parameter name, not a keyword, and it has no meaning outside a method body.
$\texttt{self.pages = pages}$
put the value of the parameter into this object's slot
Creates or replaces an instance variable. The name on the left belongs to the object and survives the call, and the name on the right is the parameter and does not.
The two names are allowed to be the same and usually are. Dropping the self part on the left makes a local name that disappears when the method returns, and nothing complains.
$\texttt{self.\_\_pages}$
the private page count of this object
Two leading underscores. Inside the class it works like any other attribute, and Python stores it under the longer name _Book__pages.
Two underscores on both sides, as in __init__, is a different thing. Those names are not renamed and are the ones Python itself calls.
$\texttt{Book.\_\_fee}$
the fee that belongs to the class rather than to an object
A class variable: one slot, written directly under the class line, shared by every object of the class.
Reading it as self.__fee also works and finds the shared value. Writing it as self.__fee = v shares nothing: it builds a slot inside that one object.
$\texttt{def \_\_repr\_\_(self):}$
the method that returns this object as text
What repr asks for, what a list or a dictionary uses when it prints its items, and what print falls back on when there is no __str__.
It must return a string. A __repr__ that prints instead of returning stops the program, because printing hands back None.
$\texttt{def \_\_lt\_\_(self, other):}$
the method behind the less than sign
What a < b runs, and what sort, sorted and min call when they need to decide an order.
Defining it gives you the less than sign and, because Python turns the comparison round, the greater than sign as well.
Conventions used here
Every output block on this page was produced by running the program.
No block of output here was predicted by eye. Each program was written to a file, run, and the characters it wrote were copied back in.
In a programming course the printed answer is the whole claim, so the page cannot itself be guessing.
What this page is allowed to use.
Everything here is built from what the course has covered by this week: numbers, text, True and False, input, print with end, format, if, while, for, range, len, def and return, the string operations, files, tuples, lists and dictionaries with their methods.
The lab sheet says that only functionality covered in the course may be used, and a solution written with a shortcut you cannot use in the exam teaches the wrong habit.
What this week adds to that list, and what it still leaves out.
From this section on, classes as well: the class line, __init__, self, private attributes, class variables, the special methods, and super with isinstance.
Knowing where the line is matters more this week than before, because a class can be written many ways and the marked one is the way the course teaches.
How a default printed form is reported.
A class with no __repr__ prints as its class name followed by an address in hexadecimal, and that address is different on every run.
An output block that cannot be reproduced is not an output block, and a student who runs it and sees a different address would be right to distrust the rest of the page.
Naming: get_name or getName.
The course uses both. Its own lab sheets ask for get_name and get_min_stock in one week and calculateFee and setAdmitDate in another, and both are accepted.
Consistency inside one answer is what matters, and the names in the specification are part of the specification.
Which of __str__ and __repr__ this page defines.
Where only one printed form is needed, this page defines __repr__ and not __str__. Two reasons: the lab sheets ask for __repr__, and a class with only __repr__ prints the same way whether the object is printed on its own or inside a list, while a class with only __str__ falls back to the address as soon as the object is in a list.
The sample runs in the lab sheets print lists of objects, so the method that has to exist is the one a list uses.
Docstrings.
Every class and every method on this page carries a docstring in the form the course uses: what the parameters are assumed to be, then what the method returns or changes.
The lab sheets require docstrings for functions, and the assumes and returns pair is where the two families of method become visible.
Private attributes are a convention.
This page says hidden and private for an attribute written with two leading underscores, which is the course's own wording.
A student told that the data cannot be reached will eventually find that it can, and will then not know which other claim on the page to trust.
8.1A class is a type you write, and an object is one thing of that type
Writing a class replaces parallel lists with one kind of thing whose parts cannot drift apart.
The three lists in the report are three objects, and the report needed one.
Solvable with what we have
Keep three facts about one book in three names and print them side by side.
Keep many books in one list, as long as a book is a single value.
Keep one book in a list of three items and index it by position.
Not solvable yet
Keep the three facts together through a sort, because sorting one list moves neither of the others.
Ask a record for something it should know, such as its reading time.
Stop a later line putting a price where a page count belongs, because position 2 means nothing on its own.
The usual repair is a list per record, so the catalogue is a list of lists. It survives a sort, and now every line reads positions:
class Book(object): starts a new type. Everything indented under it belongs to the class, and object in the brackets is the class every class comes from.
def __init__(self, title, pages, price): is the method that runs while a new object is being built.
Book('Ada', 312, 180.0) builds one object and hands it back. Python passes the new object as self and your three values as the other three parameters, which is why the call has one argument fewer than the definition has parameters.
self.title = title puts a value into a slot of this object.
Every call of the class builds a separate object with its own slots.
A parameter of __init__ may have a default, as in def __init__(self, name, staff=False):, and then a call may leave it out.
A class is itself an object: type(Book) is <class 'type'>, and the class holds the design while the objects hold the values.
$$\boxed{\texttt{class Book(object)}\ \text{is the type};\ \ \texttt{Book('Ada', 312, 180.0)}\ \text{is one object}}$$
The class says which slots every book gets and what they are called. Calling the class builds one book and fills those slots for it alone. The class holds the design, not the values.
One class, two objects. The class fixes which slots every book gets and the name of each slot; each call of Book builds a separate object with its own values in those slots, and the name on the left holds a reference to it rather than a copy.
Looks like this, but is not
Two books built from the same title and the same page count are the same book, so == should say True.
It is False, and so is is. Each call built a separate object, and for a class you write yourself == begins by asking what is asks: are these two names for one object.
The class Book, and two objects built from it
Write the class that the report needed, then build two books and print their slots. The only new lines are the class line, the definition line and the three assignments.
classBook(object):
"""One entry in the library catalogue."""def__init__(self, title, pages, price):
"""Assumes title is a str, pages an int, price a float."""self.title = title
self.pages = pages
self.price = price
b1 = Book('Ada', 312, 180.0)
b2 = Book('Zeno', 98, 75.5)
print(b1.title, b1.pages, b1.price)
print(b2.title, b2.pages, b2.price)
print(type(b1))
Sample Run:
Ada 312180.0
Zeno 9875.5
<class'__main__.Book'>
FindWhat each object holds, and what type the objects are.
Independent check on the claim that the objects are separate: b1.pages = 1 would change one line of the output and leave the other untouched, which a single shared object could not do.
Six lines of class for three lines of data. The saving starts at the third use, and the report in the hook needed four.
One kind of thing with named slots, so a later line cannot put a price where a page count belongs without saying the word price.
The shortest book and the average price, from a list of objects
This is the hook, answered. The catalogue is one list, each item is one book, and the loop asks each book for the field it needs.
classBook(object):
"""One entry in the library catalogue."""def__init__(self, title, pages, price):
"""Assumes title is a str, pages an int, price a float."""self.title = title
self.pages = pages
self.price = price
catalogue = [Book('Ada', 312, 180.0),
Book('Zeno', 98, 75.5),
Book('Milo', 204, 120.0)]
shortest = catalogue[0]
for b in catalogue:
if b.pages < shortest.pages:
shortest = b
print('shortest book:', shortest.title, 'with', shortest.pages, 'pages')
total = 0.0for b in catalogue:
total = total + b.price
print('average price:', format(total / len(catalogue), '.2f'))
Sample Run:
shortest book: Zeno with98 pages
average price: 125.17
FindThe shortest book by page count, and the average price to two decimals.
Given
The catalogue holds three Book objects.
Ada is 312 pages at 180.0, Zeno 98 at 75.5, Milo 204 at 120.0.
Solution
Hold a candidate rather than a position
$$\texttt{shortest = catalogue[0]}$$
The candidate is the object itself, not an index.
$$\texttt{if b.pages < shortest.pages:}$$
Both sides reach into their own object, so the comparison cannot read the page count of one book and the title of another.
Total, then divide once
$$\texttt{total = total + b.price}$$
Started at 0.0 rather than 0 so that the sum is a float from the first step.
$$\texttt{375.5 / 3 = 125.1\overline{6}}$$
Divided after the loop, not inside it. Dividing inside would average the averages, which is a different number as soon as the list is unevenly sized.
Independent check by size: the three prices are 75.5, 120.0 and 180.0, so the average has to sit between 75.5 and 180.0 and near the middle one. 125.17 does.
One loop for the minimum and one for the total. Both could be done in a single pass; kept apart here because each loop then answers one question and can be read on its own.
Once a record is an object, a loop over records reads like the sentence that describes the task.
Checkpoint
§08.1 — two objects, two sets of slots
Two study rooms are built from the same class and one of them is then made smaller. Thirty seconds: write what the three prints show.
classRoom(object):
"""One study room with a seat count."""def__init__(self, name, seats):
"""Assumes name is a str and seats an int."""self.name = name
self.seats = seats
small = Room('B12', 6)
big = Room('B30', 24)
big.seats = big.seats - 4print(small.name, small.seats)
print(big.name, big.seats)
print(small.seats + big.seats)
Find(a) Write the three lines this prints.
Given
Room('B12', 6) and Room('B30', 24) are two separate calls.
The line big.seats = big.seats - 4 runs between the calls and the prints.
IPython console
In [1]: %run untitled0.py
Two objects, so the subtraction reaches one of them. If the class had somehow shared its slots, the first line would have changed too, and the sum would be 4 short of 26 rather than 4 short of 30.
Hint 1/4
Before reading the change, count how many Room objects this program builds. That number decides whether the change can reach both.
Hint 2/4
Each call of a class builds a separate object with its own slots, and name.slot = value reaches into exactly one object.
Hint 3/4
Here the two calls are Room('B12', 6) and Room('B30', 24), and the change is written on big.
Hint 4/4
The small room is unchanged, the big one is four seats down, and the sum uses the new number.
Show solution
Answer the middle line first. It is the only line that changes anything, so the other two are reads of a state you already know.
Two calls, two objects
$$\texttt{small}\ \to\ \texttt{('B12', 6)}$$
Its own two slots, filled by its own call of __init__.
$$\texttt{big}\ \to\ \texttt{('B30', 24)}$$
A second object. Nothing about the first one is involved.
Apply the change where it is written
$$\texttt{big.seats = 24 - 4 = 20}$$
The dot names the object, so the slot that changes belongs to big alone.
8.2A method is a function in the class, and its first parameter is the object
Methods put the behaviour next to the data, so a book can be asked a question instead of being handed to a function.
The slots are in place, and the reading time of a book is still worked out somewhere else.
RuleRule 8.2: the method call and the self parameter
Conditions
A method is written with def inside the class body, indented like the other lines of the class.
Its first parameter is self whenever it uses the object's own data, and by convention it is written first even when the body ignores it.
b.reading_days(40) is the same call as Book.reading_days(b, 40). The object before the dot is handed in as self, which is why the call has one argument fewer than the definition has parameters.
Inside the body, self.pages reads a slot of the object the call reached and self.pages = v writes one.
A method may call another method of the same object as self.other().
The two families from the work on lists apply here too: a method either changes the object and hands back None, or leaves it alone and hands a value back.
A method may take another object of the same class as a parameter, usually called other, and read its slots directly.
Writing the object before the dot is a way of passing it as the first argument. That is the whole of what self means: the definition has a slot at the front for the object, and the call fills it from the name in front of the dot.
What the dot does to a method call. The object written before the dot is handed to the method as its first argument, which is why the definition needs one more parameter than the call has arguments, and why that first parameter is called self.
Looks like this, but is not
A method that needs nothing from the object needs no self in its parameter list.
classBook(object):
"""A book whose method was written without self."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.title = title
self.pages = pages
defchapters():
"""Returns the number of chapters, or tries to."""return12
b = Book('Ada', 312)
print(b.chapters())
Sample Run:
Traceback (most recent call last):
File "chapters.py", line 15, in <module>
print(b.chapters())
^^^^^^^^^^^^
TypeError: Book.chapters() takes 0 positional arguments but 1 was given
The call passes the object whether the method wants it or not, so a definition with no parameters receives one argument and the program stops.
One method that answers a question and one that changes the book
Two methods on the same class, one from each family. Read the docstrings first: one says returns and the other says lowers the price and returns nothing.
classBook(object):
"""A book that can answer questions about itself."""def__init__(self, title, pages, price):
"""Assumes title is a str, pages an int, price a float."""self.title = title
self.pages = pages
self.price = price
defreading_days(self, per_day):
"""Assumes per_day is an int > 0. Returns the whole days needed at per_day pages a day."""returnself.pages // per_day + 1defdiscount(self, percent):
"""Assumes percent is a number between 0 and 100. Lowers the price of this book and returns nothing."""self.price = self.price - self.price * percent / 100
b = Book('Ada', 312, 180.0)
print(b.reading_days(40), 'days at 40 pages a day')
b.discount(25)
print('new price:', format(b.price, '.2f'))
Sample Run:
8 days at 40 pages a day
new price: 135.00
FindThe two printed lines.
Given
The book starts as Ada, 312 pages, 180.0.
reading_days(40) is called before the discount.
Solution
The method that hands a value back
$$\texttt{312 // 40 = 7}$$
Whole division, because a part day of reading is still a day you have to start.
$$\texttt{7 + 1 = 8}$$
The plus one is the leftover 32 pages.
The method that changes the object
$$\texttt{180.0 - 180.0 \times 25 / 100}$$
The discount is worked out from the slot and written back to the same slot, so the object is different afterwards.
$$\texttt{= 135.0}$$
Nothing is handed back, so the call stands on a line of its own and there is nothing to assign.
Answer $$\boxed{\texttt{8 days at 40 pages a day}\ /\ \texttt{new price: 135.00}}$$
Check
Independent check on the discount: a quarter off 180 is 45 off, and 180 minus 45 is 135.
Ask of every method you write which family it is in, and put the answer in the docstring before you write the body.
A method that takes another book as its parameter
A specification often says compares this one with another. The second object arrives as an ordinary parameter, by convention named other, and its slots are read with a dot like any other object.
classBook(object):
"""A book that can compare itself with another book."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.__title = title
self.__pages = pages
defget_title(self):
"""Returns the title."""returnself.__title
defget_pages(self):
"""Returns the page count."""returnself.__pages
deflonger_than(self, other):
"""Assumes other is a Book. Returns True when this book has more pages than the other one."""returnself.__pages > other.__pages
defpages_between(self, other):
"""Assumes other is a Book. Returns the difference in page count, never a negative number."""returnabs(self.__pages - other.__pages)
first = Book('Ada', 312)
second = Book('Zeno', 98)
print(first.longer_than(second))
print(second.longer_than(first))
print(first.pages_between(second))
print(second.pages_between(first))
Independent check: 312 minus 98 is 214, and the difference of two numbers does not depend on the order once the sign is thrown away.
This is the shape of every special method in this section. Only the name changes, from longer_than to __lt__.
Checkpoint
§08.2 — which family each method is in
A water meter with one method from each family, and the first one has its value kept. Thirty seconds: write the four lines.
classMeter(object):
"""A water meter with a running reading."""def__init__(self, reading):
"""Assumes reading is an int >= 0."""self.reading = reading
defadd(self, units):
"""Assumes units is an int >= 0. Raises the reading."""self.reading = self.reading + units
defdouble(self):
"""Returns twice the reading without changing it."""returnself.reading * 2
m = Meter(120)
answer = m.add(30)
print(answer)
print(m.reading)
print(m.double())
print(m.reading)
Find(a) Write the four lines this prints.
Given
The meter starts at 120.
add has no return and double returns a number.
IPython console
In [1]: %run untitled0.py
The fourth line is the one that carries the point. double handed back 300 and the meter still reads 150, because a method that returns a value has no reason to touch the object.
Hint 1/4
Ask two questions of each method before you read any numbers: does it change the meter, and what is the call worth.
Hint 2/4
A method whose body ends without return hands back None. A method that computes and returns leaves the object as it was.
Hint 3/4
Here add(30) changes the reading and returns nothing, and double() reads the reading and returns twice it.
Hint 4/4
The first line is None, the second is the raised reading, the third is twice that, and the fourth is the reading again, unchanged.
Show solution
Take the lines in order and keep one note of the reading as you go.
The method that changes
$$\texttt{answer = m.add(30)}$$
The reading goes from 120 to 150, and the value of the call is None, which is what answer holds.
The method that returns
$$\texttt{m.double()}\ \to\ 300$$
Reads 150 and hands back twice it. Nothing is written back, so the reading is untouched.
$$\texttt{m.reading}\ \to\ 150$$
The last line proves the previous claim rather than repeating it.
Independent check: the third line has to be exactly twice the second and the fourth has to equal the second.
⚠ A method defined without self
The body does not mention the object, so the parameter looks unnecessary.
wrong$$\texttt{def chapters():}$$
right$$\texttt{def chapters(self):}$$
⚠ Calling a method of the same object without self
Inside the class the method feels like a name in scope, as a plain function would be.
wrong$$\texttt{return subtotal() * 1.10}$$
right$$\texttt{return self.subtotal() * 1.10}$$
⚠ Reaching a method without brackets
The name reads like the value it produces, and the line runs without complaint until the value is used.
wrong$$\texttt{total = b.get\_price + 20}$$
right$$\texttt{total = b.get\_price() + 20}$$
⚠ Assigning from a method that changes the object
The method clearly did something, so it looks as though it must have handed something back.
wrong$$\texttt{b = b.discount(10)}$$
right$$\texttt{b.discount(10)}$$
8.3Hidden data, and get and set methods as the only way in
Two leading underscores rename an attribute so that only code inside the class can spell it, which turns the methods into the door.
The slots so far are open to any line anywhere, and the lab sheets say every data member is private.
RuleRule 8.3: private attributes, get and set
Conditions
An attribute written self.__price inside class Book is stored under the name _Book__price.
From outside, b.__price raises an AttributeError, because no attribute of that name exists.
A get method returns the value: def get_price(self): return self.__price. One get method per data member the specification says can be read.
A set method writes it, and any condition the specification states goes inside that method: if price > 0: self.__price = price.
When the specification says to initialise a field using its set method, __init__ gives the slot a safe starting value and then calls the set method, so the condition is applied to the first value too.
A set method is in the changing family: it hands back None.
A class with get methods and no set methods is read only from outside, which is one way to make an object immutable.
The renaming is a convention and not a lock. b._Book__price = -99.0 from outside works.
$$\boxed{\texttt{self.\_\_price}\ \to\ \texttt{self.\_Book\_\_price}\ \ \text{so}\ \ \texttt{b.\_\_price}\ \text{is an AttributeError}}$$
Inside the class the short name works and outside it does not exist. That is the whole mechanism: not a guard that refuses access, but a name that only code written inside the class knows how to spell.
The double underscore is a renaming, not a lock. Inside the class the slot is called __price and Python stores it as _Book__price, so the short name only works in code that sits inside the class, and every route from outside goes through a method.
Looks like this, but is not
The price is private, so the only way to change it is the set method and the condition inside it cannot be avoided.
classBook(object):
"""A book whose hidden name we are about to read out loud."""def__init__(self, title, price):
"""Assumes title is a str and price a float."""self.__title = title
self.__price = price
defget_price(self):
"""Returns the price."""returnself.__price
b = Book('Ada', 180.0)
print(b.get_price())
b._Book__price = -99.0print(b.get_price())
Sample Run:
180.0
-99.0
The price is now negative, and no set method was called. The double underscore renamed the attribute, and writing the renamed spelling reaches it.
The class Book with private data and a set method that refuses
The specification is the one the lab sheets use: all data members private, a get method for each, a set method for the price that only accepts a positive value, and __init__ setting the price through that set method.
classBook(object):
"""A book whose data is reached only through its own methods."""def__init__(self, title, pages, price):
"""Assumes title is a str, pages an int, price a float."""self.__title = title
self.__pages = pages
self.__price = 0.0self.set_price(price)
defget_title(self):
"""Returns the title."""returnself.__title
defget_pages(self):
"""Returns the page count."""returnself.__pages
defget_price(self):
"""Returns the price."""returnself.__price
defset_price(self, price):
"""Assumes price is a number. Sets the price only when the value is positive."""if price > 0:
self.__price = price
b = Book('Ada', 312, 180.0)
print(b.get_title(), b.get_price())
b.set_price(-5.0)
print('after a negative set:', b.get_price())
b.set_price(150.0)
print('after a real set:', b.get_price())
Sample Run:
Ada 180.0
after a negative set: 180.0
after a real set: 150.0
FindThe price after each of the three stages.
Given
The book is built as Ada, 312 pages, 180.0.
set_price(-5.0) is called, then set_price(150.0).
Solution
Give the slot a value before the set method runs
$$\texttt{self.\_\_price = 0.0}$$
Written before the call to set_price. Without it, a first value that fails the condition would leave the object with no price slot at all, and the next get would raise an AttributeError instead of returning something.
$$\texttt{self.set\_price(price)}$$
Chosen over a direct assignment because the specification says to initialise through the set method, which means the condition applies to the value the caller passed in as well.
Independent check on the refusal: a set method that silently refuses can be tested by calling it and then calling the get method.
A condition stated in a specification always lands inside the set method, never at the place where the set method is called.
What reaching a private attribute from outside actually says
The error message is worth reading once in full, because it is the one this week's lab produces most often, and because the suggestion at the end of it is a hint about the renaming.
classBook(object):
"""A book with one hidden attribute and one door to it."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.__title = title
self.__pages = pages
defget_title(self):
"""Returns the title."""returnself.__title
b = Book('Ada', 312)
print(b.get_title())
print(b.__title)
Sample Run:
Ada
Traceback (most recent call last):
File "outside.py", line 16, in <module>
print(b.__title)
^^^^^^^^^
AttributeError: 'Book'object has no attribute '__title'. Did you mean: 'get_title'?
FindWhat the two lines of output are.
Given
The class stores self.__title and offers get_title.
The last line reads b.__title from outside the class.
Solution
The call that works
$$\texttt{b.get\_title()}\ \to\ \texttt{'Ada'}$$
The body of the method is inside the class, so the short name is spelled correctly there.
The read that does not
$$\texttt{b.\_\_title}$$
Written outside the class, so no renaming happens and Python looks for an attribute literally called __title.
$$\text{AttributeError}$$
The message says the object has no attribute of that name, which is true, and then suggests the nearest name it does have.
Answer $$\boxed{\texttt{Ada},\ \text{then an AttributeError}}$$
Check
Independent check: the same read written as b._Book__title returns Ada, which shows the attribute exists and only the spelling was wrong.
Checkpoint
§08.3 — a set method with a condition in it
A cafeteria card whose balance may never be negative. Thirty seconds: write the three lines, watching which calls the condition lets through.
classCard(object):
"""A cafeteria card whose balance can never go negative."""def__init__(self, owner, balance):
"""Assumes owner is a str and balance a float."""self.__owner = owner
self.__balance = 0.0self.set_balance(balance)
defget_balance(self):
"""Returns the balance."""returnself.__balance
defset_balance(self, balance):
"""Assumes balance is a number. Sets it only when the value is zero or more."""if balance >= 0:
self.__balance = balance
c = Card('Elif Karaman', 40.0)
c.set_balance(-15.0)
print(c.get_balance())
c.set_balance(0.0)
print(c.get_balance())
c.set_balance(25.5)
print(c.get_balance())
Find(a) Write the three lines this prints.
Given
The card is built with a balance of 40.0.
set_balance accepts a value only when it is zero or more.
IPython console
In [1]: %run untitled0.py
The middle line is the one that separates a careful reader from a quick one. Zero passes, because the condition is greater than or equal rather than greater than, so the balance really does become 0.0.
Hint 1/4
Take the condition one call at a time and note what the balance is after each, rather than reading all three calls and then deciding.
Hint 2/4
When the if inside a set method is False, the body does not run: the old value stays and nothing is raised, so a refused call looks exactly like a call that never happened.
Hint 3/4
Here the condition is balance >= 0, and the three values offered are -15.0, then 0.0, then 25.5.
Hint 4/4
The first print shows the starting balance, the second shows zero, and the third shows the last accepted value.
Show solution
Work forwards through the three calls with one running note of the balance.
The refused call
$$\texttt{set\_balance(-15.0)}$$
The condition is False, so the slot keeps 40.0 from the construction.
The two accepted calls
$$\texttt{set\_balance(0.0)}\ \to\ 0.0$$
Zero satisfies greater than or equal to zero, so this one is written.
$$\texttt{set\_balance(25.5)}\ \to\ 25.5$$
Plainly accepted, and it replaces the zero.
Answer $$\boxed{40.0,\ 0.0,\ 25.5}$$
Check
Independent check: a condition written > 0 instead of >= 0 would print 40.0 twice, so the middle line is what tells the two versions apart.
⚠ Putting the condition at the call rather than in the set method
The check reads naturally where the value is known, and it works, until a second place in the program sets the value.
⚠ Reading a private attribute from outside the class
Inside the class the short name works, so it looks like the name of the attribute rather than a spelling only the class knows.
wrong$$\texttt{print(b.\_\_price)}$$
right$$\texttt{print(b.get\_price())}$$
⚠ One underscore instead of two
Both look private in a listing, and the single underscore version quietly works from outside, so nothing complains until the specification is marked.
wrong$$\texttt{self.\_price = price}$$
right$$\texttt{self.\_\_price = price}$$
8.4A class variable is one slot for the whole class
A fee or a capacity that every object shares belongs to the class, and reading it through self works while writing it through self does not.
Some of what a specification lists is not a property of one object at all: the lab sheet gives every patient the same hospital fee.
RuleRule 8.4: class variable against instance variable
Conditions
A class variable is written directly under the class line, outside every method: __fine_per_day = 2.5.
An instance variable is created by an assignment to self.something inside a method, usually __init__.
Read a class variable through the class name: Book.__fine_per_day. Reading it as self.__fine_per_day also finds it, because Python looks in the object first and then in the class.
Write a class variable through the class name too: Book.__fine_per_day = v.
After such a write, that one object reads its own value and every other object still reads the shared one, so a method that reads through the class name reports no change at all.
A class variable whose value is a list or a dictionary is shared along with everything in it, so an append made through one object is visible through all of them.
The same double underscore renaming applies: __fine_per_day inside class Book is stored as _Book__fine_per_day.
$$\boxed{\texttt{Book.\_\_fee = v}\ \text{changes the shared slot};\ \ \texttt{self.\_\_fee = v}\ \text{makes a new one}}$$
A read through self goes looking in the object and then in the class, so it finds the shared value. A write through self stops at the object and puts the value there, so it never reaches the class.
A class variable sits in one place and every object reaches the same one, which is what makes it useful and what makes writing to it through self go wrong: that line builds a new slot inside one object and leaves the shared value where it was.
Looks like this, but is not
A method that raises the shared fine can be written with self on the left, because self is the object and the object is a Book.
classBook(object):
"""A book whose method writes the shared fine through self."""
__fine_per_day = 2.5def__init__(self, title):
"""Assumes title is a str."""self.__title = title
defraise_fine(self, amount):
"""Assumes amount is a number. Meant to raise the shared fine."""self.__fine_per_day = Book.__fine_per_day + amount
deffine(self, days_late):
"""Assumes days_late is an int >= 0. Returns the fine owed."""return days_late * Book.__fine_per_day
b1 = Book('Ada')
b2 = Book('Zeno')
b1.raise_fine(1.5)
print(b1.fine(2), b2.fine(2))
print(b1._Book__fine_per_day, Book._Book__fine_per_day)
Sample Run:
5.05.04.02.5
Both fines are still 5.0, so the raise did nothing that any reader can see. The last line shows what really happened: the first book now carries its own value of 4.0 while the class still holds 2.5, and the method reads the class.
Raising the shared fine, written the way that works
The same class with one word changed in the method that raises the fine. Compare the output with the counterexample above, where the left hand side was self.
classBook(object):
"""A book whose method writes the shared fine on the class."""
__fine_per_day = 2.5def__init__(self, title):
"""Assumes title is a str."""self.__title = title
defraise_fine(self, amount):
"""Assumes amount is a number. Raises the fine every book shares."""
Book.__fine_per_day = Book.__fine_per_day + amount
deffine(self, days_late):
"""Assumes days_late is an int >= 0. Returns the fine owed."""return days_late * Book.__fine_per_day
b1 = Book('Ada')
b2 = Book('Zeno')
b1.raise_fine(1.5)
print(b1.fine(2), b2.fine(2))
Sample Run:
8.08.0
FindThe fine each of the two books reports for two days.
The name on the left is the class slot, so the one shared value is replaced.
Check both objects, not one
$$\texttt{2 \times 4.0 = 8.0}\ \text{for b1}$$
2.5 plus 1.5 is 4.0, and two days of it is 8.0.
$$\texttt{8.0}\ \text{for b2 as well}$$
The book that was never touched reports the new fine, which is the whole point of a shared value and the thing the counterexample failed to do.
Answer $$\boxed{\texttt{8.0 8.0}}$$
Check
Independent check against the counterexample: the same program with self on the left prints 5.0 5.0.
A method that is meant to change something shared says the class name out loud on the left of the assignment.
A list written under the class line is one list for everybody
Two shelves, each given one title, and the two titles end up on both. This is the aliasing problem from the work on lists, arriving through a class.
classShelf(object):
"""A shelf of titles. The list was created in the wrong place."""
__titles = []
defadd(self, title):
"""Assumes title is a str. Puts it on this shelf."""
Shelf.__titles.append(title)
defget_titles(self):
"""Returns the titles on this shelf."""return Shelf.__titles
upstairs = Shelf()
downstairs = Shelf()
upstairs.add('Ada')
downstairs.add('Zeno')
print(upstairs.get_titles())
print(downstairs.get_titles())
print(upstairs.get_titles() is downstairs.get_titles())
Sample Run:
['Ada', 'Zeno']
['Ada', 'Zeno']
True
The repair is one line moved: the list is built in __init__, so each shelf gets its own.
classShelf(object):
"""A shelf of titles. Each shelf builds its own list."""def__init__(self):
"""Starts this shelf with an empty list of its own."""self.__titles = []
defadd(self, title):
"""Assumes title is a str. Puts it on this shelf."""self.__titles.append(title)
defget_titles(self):
"""Returns the titles on this shelf."""returnself.__titles
upstairs = Shelf()
downstairs = Shelf()
upstairs.add('Ada')
downstairs.add('Zeno')
print(upstairs.get_titles())
print(downstairs.get_titles())
print(upstairs.get_titles() is downstairs.get_titles())
Sample Run:
['Ada']
['Zeno']
False
FindWhat each shelf reports in each version, and what is says.
Given
In the first version __titles = [] is written under the class line.
In the second it is self.__titles = [] inside __init__.
Each version adds one title through each of two objects.
Solution
Count the lists in the first version
$$\texttt{\_\_titles = []}\ \text{under the class line}$$
Evaluated once, when the class definition is read, so there is exactly one list for the whole program.
Each shelf holds what it was given. The False is the same check as before and now answers the other way.
Answer $$\boxed{\text{shared: two titles on both};\ \ \text{own: one title each}}$$
Check
Independent check by length: in the shared version both lists report a length of 2 after two single appends, which is one more than any one shelf was given.
One line moved, from under the class line into __init__. Every specification in this week's labs that says initialise an empty list of stocks means the second version.
Anything whose value is a list or a dictionary belongs in __init__ unless the specification really does mean one collection for the whole class.
Checkpoint
§08.4 — shared price, separate counters
Two campus printers. The price per page is written under the class line and the page count is set in __init__.
classPrinter(object):
"""A campus printer. The page price is the same everywhere."""
__page_price = 0.4def__init__(self, place):
"""Assumes place is a str."""self.__place = place
self.__pages = 0defprint_pages(self, pages):
"""Assumes pages is an int >= 0. Adds them to this printer."""self.__pages = self.__pages + pages
defowed(self):
"""Returns what this printer has taken in."""returnself.__pages * Printer.__page_price
library = Printer('library')
dorm = Printer('dorm')
library.print_pages(12)
dorm.print_pages(5)
print(format(library.owed(), '.2f'), format(dorm.owed(), '.2f'))
print(library.owed() == dorm.owed())
Find(a) Write the two lines this prints.
Given
__page_price is 0.4 and is written under the class line.
__pages starts at 0 for each printer.
IPython console
In [1]: %run untitled0.py
The shared value is the price, so both printers charge 0.4 a page. The counts are not shared, so the amounts differ: 12 times 0.4 and 5 times 0.4.
Hint 1/4
Sort the two attributes into the two kinds before you compute anything: one of them is one slot for the class and the other is one slot per object.
Hint 2/4
A name assigned under the class line is shared. A name assigned to self.something inside a method belongs to that object alone.
Hint 3/4
Here the price of 0.4 is shared by both printers, and the page counts of 12 and 5 belong to one printer each.
Hint 4/4
The two amounts owed are different, and the comparison on the second line is False.
Show solution
Decide which attribute is shared before doing the arithmetic. Doing the arithmetic first makes both answers look like plain multiplication and hides the question being asked.
Sort the two attributes
$$\texttt{\_\_page\_price}\ \text{on the class}$$
One slot, written under the class line, never assigned through self anywhere in the program.
$$\texttt{\_\_pages}\ \text{on each object}$$
Assigned in __init__, so each printer starts at 0 and counts only its own pages.
Multiply each count by the one price
$$\texttt{12 \times 0.4 = 4.8}$$
The library printer, formatted to two decimals as 4.80.
$$\texttt{5 \times 0.4 = 2.0}$$
The dorm printer, and the two being unequal is what the second line reports.
Without a printed form an object prints as an address, and the method a list uses is __repr__ rather than __str__.
Everything so far has printed the slots one at a time, and every sample run in the lab sheets prints the object.
RuleRule 8.5: __repr__, __str__, and what each is for
Conditions
def __repr__(self): must return a string. It is what repr(b) asks for, what a list or a dictionary uses when it prints its items, and what print(b) falls back on when there is no __str__.
def __str__(self): must also return a string. It is what print(b) and str(b) ask for first.
A class with only __str__ still prints as an address inside a list, because a list asks each item for its __repr__.
A class with only __repr__ prints the same text in both places, which is why the lab sheets ask for that one.
The convention the course states: str is the friendly form for a reader, repr is the exact form for whoever is fixing the program.
Every value put into the returned string has to be a string already: str(self.__pages) for a whole number, format(self.__price, '.2f') for money.
A __repr__ that prints instead of returning stops the program, because a print hands back None and the message says that __str__ returned a non-string.
Newlines inside the returned string are part of it. A form ending in a newline, printed with print, leaves a blank line after it, and that blank line is in the lab sample runs.
Printing an object on its own asks for the friendly form first and falls back to the exact one. Printing a container of objects asks each item for the exact form and never for the friendly one.
Which method each way of printing reaches. The print statement and str look for __str__ first and fall back to __repr__, while repr and every container go straight to __repr__, and a class with neither prints a line that ends in an address.
Looks like this, but is not
The job of __repr__ is to show the object, so printing inside it is one way to do that.
classBook(object):
"""A book whose printed form prints instead of returning."""def__init__(self, title):
"""Assumes title is a str."""self.__title = title
def__repr__(self):
"""Meant to give the title back, but it prints it."""print('Title: ' + self.__title)
b = Book('Ada')
print(b)
Sample Run:
Title: Ada
Traceback (most recent call last):
File "shownow.py", line 14, in <module>
print(b)
TypeError: __str__ returned non-string (type NoneType)
The title does appear, and then the program stops. The method printed and returned None, and print needs a string back. The message names __str__ even though the method written was __repr__, because print asked for the string form and was handed the fallback.
A three line block for one book, and what a list of them looks like
The format is the one the lab sample runs use: a labelled line per field, ending in a newline. Then the same two books printed as a list, which is where the sample runs get their odd looking brackets and commas.
classBook(object):
"""A book that knows how to print itself."""def__init__(self, title, author, pages):
"""Assumes title and author are str, pages an int."""self.__title = title
self.__author = author
self.__pages = pages
defget_title(self):
"""Returns the title."""returnself.__title
def__repr__(self):
"""Returns the three line block the catalogue prints."""return ('Title: ' + self.__title + '\n'
+ 'Author: ' + self.__author + '\n'
+ 'Pages: ' + str(self.__pages) + '\n')
b1 = Book('Ada', 'Kara', 312)
b2 = Book('Zeno', 'Acar', 98)
print(b1)
print([b1, b2])
Sample Run:
Title: Ada
Author: Kara
Pages: 312
[Title: Ada
Author: Kara
Pages: 312
, Title: Zeno
Author: Acar
Pages: 98
]
FindThe exact output, blank lines included.
Given
__repr__ returns three labelled lines and ends with a newline.
The first print takes one object and the second takes a list of two.
The str is not optional: adding an int to a string stops the program.
$$\texttt{print(b1)}$$
Three lines from the method plus the newline print adds of its own, so there is one blank line after the block.
Let the list print itself
$$\texttt{print([b1, b2])}$$
The list asks each item for its __repr__ and puts the results between brackets with a comma between them.
$$\texttt{,}\ \text{on a line of its own}$$
Each returned string already ends in a newline, so the comma the list adds starts the next line.
Answer $$\boxed{\text{three labelled lines, a blank line, then the same two blocks inside brackets}}$$
Check
Independent check on the field order: the labels in the output appear in the order the concatenation puts them, Title then Author then Pages, and swapping two lines of the method swaps two lines of every block.
One method, four lines, and every print of a book anywhere in the program now matches the sample run.
When a sample run shows brackets and commas around blocks of text, the script printed a list and the class had a __repr__.
Both forms on one class, and which one each print reaches
The one case where writing both is worth it: a short friendly line for a reader and an exact form for debugging. Four prints, two of which reach the same method.
classBook(object):
"""A book with two printed forms."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.__title = title
self.__pages = pages
def__str__(self):
"""Returns the form meant for whoever is reading the screen."""returnself.__title + ' (' + str(self.__pages) + ' pages)'def__repr__(self):
"""Returns the form meant for whoever is fixing the program."""return'Book(' + repr(self.__title) + ', ' + repr(self.__pages) + ')'
b = Book('Ada', 312)
print(b)
print(str(b))
print(repr(b))
print([b, b])
Sample Run:
Ada (312 pages)
Ada (312 pages)
Book('Ada', 312)
[Book('Ada', 312), Book('Ada', 312)]
FindWhich of the four lines comes from which method.
Given
__str__ returns the title with the page count in brackets.
__repr__ returns something that looks like the call that built it.
Independent check on which method a container uses: delete the __str__ method and the first two lines change while the last two do not.
Write __repr__ when a specification asks for one printed form. Add __str__ only when a reader needs something shorter than the exact form.
Checkpoint
§08.5 — one object, then a list of them
A seat in a lecture hall, with a printed form of one line. Thirty seconds: write the three lines, remembering which method a list uses.
classSeat(object):
"""One seat in a lecture hall."""def__init__(self, row, letter):
"""Assumes row is an int and letter a str."""self.__row = row
self.__letter = letter
def__repr__(self):
"""Returns the row and the letter with no space between them."""returnstr(self.__row) + self.__letter
front = Seat(1, 'A')
back = Seat(14, 'F')
print(front)
print([front, back])
print(str(back) + ' taken')
Find(a) Write the three lines this prints.
Given
__repr__ returns the row number joined to the letter.
The seats are row 1 letter A, and row 14 letter F.
IPython console
In [1]: %run untitled0.py
All three routes reached the same method, because there is no __str__ to reach first. The middle line is the one to remember: a list shows the __repr__ of each item, without quotation marks, because what it prints is the text the method returned rather than a string value.
Hint 1/4
Ask, for each of the three prints, which method it ends up calling.
Hint 2/4
print(b) asks for __str__ and falls back to __repr__. A list asks each item for __repr__.
Hint 3/4
Here there is no __str__, so all three routes end at the one __repr__, which returns the row number and the letter with nothing between them.
Hint 4/4
The second line has the two forms inside brackets with a comma between them, and the third has a word added after the seat.
Show solution
Decide the route for each print before working out any text.
What the one method returns
$$\texttt{str(1) + 'A'}\ \to\ \texttt{1A}$$
The row number is an int, so it needs str before the join.
Three routes to it
$$\texttt{print(front)}$$
No __str__, so it falls back to __repr__.
$$\texttt{print([front, back])}$$
The list asks each item for __repr__ and puts a comma between the results.
8.6Special methods: making the operators and sort work on your objects
The operators and the built in functions are calls to methods with fixed names, so writing one of those names is how a class joins in.
A method called longer_than works and nothing else in Python knows about it, so sort on a list of books still refuses.
RuleRule 8.6: the operator is the method
Conditions
Each operator has a fixed method name behind it: x + y is __add__, x - y is __sub__, x * y is __mul__, x / y is __truediv__, x // y is __floordiv__, x % y is __mod__, x ** y is __pow__.
The comparisons follow the same pattern: == is __eq__, != is __ne__, < is __lt__, <= is __le__, > is __gt__, >= is __ge__.
Every one of them takes self and one other object, by convention named other, and returns a value.
L.sort(), sorted(L) and min(L) decide the order by calling __lt__, so defining that one method is what makes a list of objects sortable.
e in L, L.index(e) and L.count(e) decide what counts as the same item by calling __eq__.
Defining __lt__ also gives you the greater than sign, because Python answers a > b by asking b < a.
A two part order, such as by count and then by name, is two tests inside __lt__: the second one runs only when the first field is equal.
An arithmetic special method should return a new object rather than change either of the two it was given, which is what makes c = a + b behave the way it does for numbers.
There is no separate machinery behind the operators. A comparison is a method call with a fixed name, and so is a membership test, and so is the decision a sort makes about two items.
What sort is really doing to a list of objects: every decision it makes is one call of __lt__, and the pair with the same page count is the one that shows whether the second test in the method is there or not.
Looks like this, but is not
Once __lt__ is written the class knows how to be ordered, so all four comparison signs should work.
classDuration(object):
"""A length of time that only knows what less than means."""def__init__(self, hours, minutes):
"""Assumes hours and minutes are ints >= 0."""self.__hours = hours + minutes // 60self.__minutes = minutes % 60def__lt__(self, other):
"""Assumes other is a Duration. True when this one is shorter."""return (self.__hours * 60 + self.__minutes
< other.__hours * 60 + other.__minutes)
def__repr__(self):
"""Returns hours and minutes, two digits each."""return'{0:02d}:{1:02d}'.format(self.__hours, self.__minutes)
lab = Duration(1, 50)
lecture = Duration(2, 25)
print(lab < lecture)
print(lab > lecture)
print(lab <= lecture)
Sample Run:
TrueFalse
Traceback (most recent call last):
File "compare.py", line 23, in <module>
print(lab <= lecture)
^^^^^^^^^^^^^^
TypeError: '<='not supported between instances of 'Duration'and'Duration'
The first two lines work and the third stops the program. Python answers a greater than by asking the other object the less than question, so that one comes free.
Ordering books by page count, with the title as tie breaker
This is the shape the lab sheets ask for: shorter first, and when two counts are equal the titles decide. Two of the four books have 204 pages on purpose, because that is the pair the second test is for.
classBook(object):
"""A book that compares by page count, then by title."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.__title = title
self.__pages = pages
defget_title(self):
"""Returns the title."""returnself.__title
defget_pages(self):
"""Returns the page count."""returnself.__pages
def__lt__(self, other):
"""Assumes other is a Book. Returns True when this book is shorter, and when the two counts are equal, when this title comes first in alphabetical order."""ifself.__pages < other.__pages:
returnTrueifself.__pages == other.__pages andself.__title < other.__title:
returnTruereturnFalsedef__repr__(self):
"""Returns the title and the page count on one line."""returnself.__title + '/' + str(self.__pages)
shelf = [Book('Milo', 204), Book('Ada', 312), Book('Zeno', 98),
Book('Bora', 204)]
print(shelf[0] < shelf[1])
shelf.sort()
print(shelf)
print(min(shelf))
Independent check by hand on the tie: 204 equals 204, so the method compares 'Bora' < 'Milo', which is True because B comes before M.
Seven lines of method, and now sort, sorted, min and the greater than sign all work on books.
When a specification describes an order in two sentences, the method has two tests, and the second one is guarded by an equality.
Adding two durations and getting a third
An arithmetic special method should build a new object and leave both of its arguments alone, exactly as adding two numbers does. The carry from minutes into hours is done in __init__, so it happens wherever a duration is built.
classDuration(object):
"""A length of time in whole hours and minutes."""def__init__(self, hours, minutes):
"""Assumes hours and minutes are ints >= 0. Sixty minutes or more are carried into the hours."""self.__hours = hours + minutes // 60self.__minutes = minutes % 60defget_hours(self):
"""Returns the whole hours."""returnself.__hours
defget_minutes(self):
"""Returns the minutes left over."""returnself.__minutes
def__add__(self, other):
"""Assumes other is a Duration. Returns a NEW Duration."""return Duration(self.__hours + other.__hours,
self.__minutes + other.__minutes)
def__lt__(self, other):
"""Assumes other is a Duration. Returns True when this length is the shorter one."""return (self.__hours * 60 + self.__minutes
< other.__hours * 60 + other.__minutes)
def__repr__(self):
"""Returns hours and minutes, two digits each."""return'{0:02d}:{1:02d}'.format(self.__hours, self.__minutes)
lab = Duration(1, 50)
lecture = Duration(2, 25)
print(lab, lecture)
print(lab + lecture)
print(lab < lecture)
week = [Duration(0, 45), Duration(3, 5), Duration(1, 50)]
week.sort()
print(week)
Sample Run:
01:5002:2504:15True
[00:45, 01:50, 03:05]
FindThe four printed lines.
Given
The lab lasts 1 hour 50 and the lecture 2 hours 25.
__init__ carries 60 minutes or more into the hours.
The week list holds 45 minutes, 3 hours 5 and 1 hour 50.
Independent check in minutes: 110 plus 145 is 255, and 255 minutes is 4 hours and 15 minutes, which is what the third line shows.
Three special methods and one constructor. The addition is two lines because the carry was already paid for in __init__.
Put a conversion or a carry in the constructor, and every method that builds an object of the class inherits it for free.
Checkpoint
§08.6 — a sort with one tie in it
A tournament table ordered by points, with the name as tie breaker. Thirty seconds: write the two lines, and watch the pair with equal points.
classTeam(object):
"""A tournament team with a points total."""def__init__(self, name, points):
"""Assumes name is a str and points an int."""self.__name = name
self.__points = points
def__lt__(self, other):
"""Assumes other is a Team. Returns True when this team has fewer points, and when the totals are equal, when this name comes first in alphabetical order."""ifself.__points < other.__points:
returnTrueifself.__points == other.__points andself.__name < other.__name:
returnTruereturnFalsedef__repr__(self):
"""Returns the name and the points on one line."""returnself.__name + '(' + str(self.__points) + ')'
table = [Team('Kartal', 7), Team('Ada', 7), Team('Zeytin', 3)]
table.sort()
print(table)
print(table[0] < table[1])
Find(a) Write the two lines this prints.
Given
The teams are Kartal on 7, Ada on 7 and Zeytin on 3.
__lt__ compares points first, then names when the points are equal.
IPython console
In [1]: %run untitled0.py
Fewest points first, so Zeytin leads. Ada comes before Kartal because the points are equal and the tie breaker compares the names, which is the only part of this a version without the second test would get wrong.
Hint 1/4
Find the pair the tie breaker is for before sorting anything. There is exactly one such pair here and it decides the whole answer.
Hint 2/4
A sort puts the smallest first, and the method says smaller means fewer points, or the same points and a name earlier in the alphabet.
Hint 3/4
Here Kartal and Ada both have 7 points, so for that pair the method compares the names, and Zeytin has 3.
Hint 4/4
Zeytin is first, then the two tied teams in alphabetical order, and the comparison on the second line is True.
Show solution
Sort by the main field first and settle the tie afterwards.
Order by the main field
$$3 < 7$$
Zeytin is smallest by points and nothing can move it, because the tie breaker only runs when the points are equal.
Settle the one tie
$$\texttt{'Ada' < 'Kartal'}\ \to\ \texttt{True}$$
Both on 7 points, so the second test runs and A comes before K.
$$\texttt{table[0] < table[1]}$$
After the sort the first two are Zeytin and Ada, so this is 3 against 7 and the answer is True.
Independent check: the list as written had Kartal before Ada, so the printed order is not the order they were given, and only the tie breaker could have swapped them.
⚠ Sorting a list of objects with no comparison method
Sorting a list of numbers or strings needs nothing, so it looks as though sorting is a property of the list.
The specification mentions the second field, so a second test looks like the whole requirement.
wrong$$\texttt{return self.\_\_p < other.\_\_p or self.\_\_n < other.\_\_n}$$
right$$\texttt{if self.\_\_p == other.\_\_p and self.\_\_n < other.\_\_n:}$$
⚠ Expecting <= from __lt__
The greater than sign really does come free, so the other two look as though they must as well.
wrong$$\texttt{if a <= b:}\ \text{with only}\ \texttt{\_\_lt\_\_}$$
right$$\texttt{if not (b < a):}\ \text{, or write}\ \texttt{\_\_le\_\_}$$
8.7Extending a class: a subclass, an override, and where a call lands
A subclass keeps everything the parent has, adds what it needs, and replaces the methods whose behaviour has to change.
An audio book is a catalogue entry with two extra fields and one answer that has to change, and copying the whole class to get that is the thing to avoid.
RuleRule 8.7: subclass, override, and the search order
Conditions
class AudioBook(Book): makes a subclass. It has every method the parent has, and it may add new ones and replace existing ones.
A call is looked up in the class of the object first, then in its parent, then in the parent's parent, up to object.
Replacing a method by writing one of the same name in the subclass is overriding.
A subclass with its own __init__ must set up the parent's data too, with super().__init__(...) as its first line.
An inherited method that calls self.something() reaches the subclass version if there is one.
A subclass that adds nothing is written with pass as its body.
isinstance(a, Book) is True for an object of Book and for an object of any subclass of it, so an AudioBook is also a Book.
A private attribute of the parent cannot be reached from inside the subclass, because the two underscores are renamed with the name of the class the line is written in.
Python asks the object's own class first and walks up only when it finds nothing. So an override is not a replacement of the parent method but a shorter path to a different one, and a method the subclass does not mention is answered by the parent on the subclass object.
Where a call lands. Python looks in the class of the object first and walks up the arrows until it finds a method of that name, so an overridden method stops the search at the subclass and everything not written there is answered by the parent.
Looks like this, but is not
A subclass inherits the parent's data, so a method of the subclass can read self.__pages the way the parent does.
classBook(object):
"""A catalogue entry whose page count is hidden."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.__title = title
self.__pages = pages
defget_pages(self):
"""Returns the page count."""returnself.__pages
classAudioBook(Book):
"""A catalogue entry that reaches for the hidden page count."""def__init__(self, title, pages, minutes):
"""Assumes minutes is an int."""super().__init__(title, pages)
self.__minutes = minutes
defpages_per_minute(self):
"""Returns how many pages one minute of audio covers."""returnself.__pages / self.__minutes
a = AudioBook('Zeno', 98, 205)
print(a.get_pages())
print(a.pages_per_minute())
Sample Run:
98
Traceback (most recent call last):
File "audio.py", line 29, in <module>
print(a.pages_per_minute())
^^^^^^^^^^^^^^^^^^^^
File "audio.py", line 24, in pages_per_minute
returnself.__pages / self.__minutes
^^^^^^^^^^^^
AttributeError: 'AudioBook'object has no attribute '_AudioBook__pages'. Did you mean: '_AudioBook__minutes'?
The inherited get method works, so the page count is certainly there. The line inside the subclass asks for _AudioBook__pages, because the renaming uses the class the line is written in, and the slot is called _Book__pages.
AudioBook extends Book: two new fields, one overridden answer
The shape the lab sheet asks for. The child calls the parent's __init__, adds its two fields, overrides the method whose answer has to change, and builds its printed form on top of the parent's one.
classBook(object):
"""A catalogue entry with a title, an author and a page count."""def__init__(self, title, author, pages):
"""Assumes title and author are str, pages an int."""self.__title = title
self.__author = author
self.__pages = pages
defget_title(self):
"""Returns the title."""returnself.__title
defget_pages(self):
"""Returns the page count."""returnself.__pages
defreading_hours(self):
"""Returns the whole hours needed at 40 pages an hour."""returnself.__pages // 40def__repr__(self):
"""Returns two lines: the title with its author, then the length."""return ('Title: ' + self.__title + ' by ' + self.__author + '\n'
+ 'Reading time: ' + str(self.reading_hours()) + ' h\n')
classAudioBook(Book):
"""A catalogue entry that is listened to rather than read."""def__init__(self, title, author, pages, minutes, reader):
"""Assumes minutes is an int and reader a str. The title, author and pages are set by the Book __init__."""super().__init__(title, author, pages)
self.__minutes = minutes
self.__reader = reader
defget_reader(self):
"""Returns the name of the person reading it aloud."""returnself.__reader
defreading_hours(self):
"""Returns the whole hours of audio, replacing the page estimate."""returnself.__minutes // 60def__repr__(self):
"""Returns the Book lines, then the reader."""returnsuper().__repr__() + 'Read by: ' + self.__reader + '\n'
paper = Book('Ada', 'Kara', 312)
audio = AudioBook('Zeno', 'Acar', 98, 205, 'Deniz Ok')
print(paper)
print(audio)
print(audio.get_title(), audio.get_pages(), audio.get_reader())
print(paper.reading_hours(), audio.reading_hours())
print(Book.reading_hours(audio))
Sample Run:
Title: Ada by Kara
Reading time: 7 h
Title: Zeno by Acar
Reading time: 3 h
Read by: Deniz Ok
Zeno 98 Deniz Ok
732
FindThe five printed lines, and in particular the reading time inside the audio book's block.
Given
Ada is a paper book of 312 pages; Zeno is an audio book of 98 pages and 205 minutes, read by Deniz Ok.
Book.reading_hours divides the pages by 40 and AudioBook.reading_hours divides the minutes by 60.
First line of the child's __init__. Chosen over three assignments of its own, which would not work at all: the parent's slots are private to the parent and only its own code can spell them.
$$\texttt{self.\_\_minutes = minutes}$$
The two new fields afterwards. These belong to the subclass and the parent knows nothing about them.
The parent's version is still there and can be asked for by name.
Answer $$\boxed{\text{Ada: 7 h};\ \ \text{Zeno: 3 h and read by Deniz Ok};\ \ \texttt{7 3}\ \text{then}\ 2}$$
Check
Independent check by arithmetic: 312 divided by 40 is 7 whole hours, 205 minutes is 3 whole hours, and 98 pages by the paper rule would be 2.
The child is fourteen lines and repeats none of the parent's twenty. Two of those lines, the super calls, are what buys that.
An inherited method that calls another method through self is the mechanism behind most exam questions on inheritance.
isinstance, a subclass with pass, and a comparison across two classes
Three classes and six questions. Reserve adds nothing and exists so that a reserve copy can be told from an ordinary one; both subclasses are still Books.
classBook(object):
"""A catalogue entry that compares by title."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.__title = title
self.__pages = pages
defget_title(self):
"""Returns the title."""returnself.__title
def__lt__(self, other):
"""Assumes other is a Book. Compares the titles alphabetically."""returnself.__title < other.__title
classAudioBook(Book):
"""A catalogue entry that compares by how long the audio runs."""def__init__(self, title, pages, minutes):
"""Assumes minutes is an int."""super().__init__(title, pages)
self.__minutes = minutes
def__lt__(self, other):
"""Assumes other is an AudioBook. Compares the audio lengths."""returnself.__minutes < other.__minutes
classReserve(Book):
"""A copy that may not leave the building. Nothing new is added."""pass
paper = Book('Milo', 204)
short = AudioBook('Ada', 98, 205)
long_one = AudioBook('Zeno', 312, 640)
desk = Reserve('Bora', 150)
print(isinstance(short, AudioBook), isinstance(short, Book))
print(isinstance(paper, AudioBook))
print(isinstance(desk, Book), isinstance(desk, Reserve))
print(short < long_one)
print(paper < short)
print(desk.get_title())
Sample Run:
TrueTrueFalseTrueTrueTrueFalse
Bora
FindThe six printed lines.
Given
Book.__lt__ compares titles and AudioBook.__lt__ compares minutes.
Reserve inherits from Book with pass.
The objects are Book Milo, AudioBook Ada at 205 minutes, AudioBook Zeno at 640, and Reserve Bora.
Independent check on the fifth line: it is the left hand object that decides which method is asked first, so swapping the comparison to short < paper asks AudioBook's version instead.
In a comparison the left hand object chooses the method. That is harmless while both sides are of the same class and is a trap as soon as they are not.
Two dates, a class variable, and a field that starts as None
The lab sheet for this week uses dates, and this is the pattern it asks for: a date arrives as text and is turned into a date object, a second date starts as None until it is set, and the number of days between two dates is (later - earlier).days.
import datetime
classLoan(object):
"""One loan, with the day it went out and the day it came back."""
__daily_fine = 2.0def__init__(self, title, taken):
"""Assumes title is a str and taken a date written as YYYYmmdd."""self.__title = title
self.__taken = datetime.datetime.strptime(taken, '%Y%m%d').date()
self.__returned = Nonedefget_taken(self):
"""Returns the day the book went out."""returnself.__taken
defset_returned(self, returned):
"""Assumes returned is a date written as YYYYmmdd."""self.__returned = datetime.datetime.strptime(returned,
'%Y%m%d').date()
defdays_out(self):
"""Returns the days the book was out, and zero while it is still out."""ifself.__returned isNone:
return0return (self.__returned - self.__taken).days
deffine(self):
"""Returns the fine owed for each day past the fourteenth."""
late = self.days_out() - 14if late <= 0:
return0.0return late * Loan.__daily_fine
def__repr__(self):
"""Returns the four line block the desk prints."""return ('Title: ' + self.__title + '\n'
+ 'Taken: ' + str(self.__taken) + '\n'
+ 'Returned: ' + str(self.__returned) + '\n'
+ 'Fine: ' + format(self.fine(), '.2f') + '\n')
one = Loan('Ada', '20241011')
one.set_returned('20241102')
two = Loan('Zeno', '20241017')
print(one)
print(two)
print(one.days_out(), two.days_out())
Done in __init__ rather than at every use, so the rest of the class works with dates and never with text.
$$\texttt{self.\_\_returned = None}$$
None is the honest starting value for a date that has not happened.
Subtract two dates
$$\texttt{(returned - taken).days}\ \to\ 22$$
Subtracting two dates gives an object whose days field is the difference.
$$\texttt{22 - 14 = 8}\ \text{days late}$$
The loan period comes off first, so the fine counts only the days past it.
Answer $$\boxed{\text{Ada: 22 days, fine 16.00};\ \ \text{Zeno: 0 days, fine 0.00}}$$
Check
Independent check by counting on a calendar: October has 31 days, so from the eleventh to the thirty first is 20 days, and two more reaches the second of November. 22 days, of which 8 are past a two week loan, at 2.0 each, is 16.00.
One import and two conversions, and every date question in the lab becomes a subtraction.
Checkpoint
§08.7 — an inherited printed form over an overridden method
A loan fee and a reserve loan fee. The child overrides the amount and writes no printed form of its own.
classFee(object):
"""The plain fee for one library loan."""def__init__(self, days):
"""Assumes days is an int >= 0."""self.__days = days
defget_days(self):
"""Returns how many days the loan ran over."""returnself.__days
defamount(self):
"""Returns the fee at two lira a day."""returnself.__days * 2def__repr__(self):
"""Returns the fee with the word lira after it."""returnstr(self.amount()) + ' lira'classReserveFee(Fee):
"""The fee for a reserve loan, which is charged by the hour."""def__init__(self, days, hours):
"""Assumes days and hours are ints >= 0."""super().__init__(days)
self.__hours = hours
defamount(self):
"""Returns the fee at five lira an hour."""returnself.__hours * 5
plain = Fee(3)
reserve = ReserveFee(3, 4)
print(plain)
print(reserve)
print(reserve.get_days())
Find(a) Write the three lines this prints.
Given
Fee.amount() is two lira for each of the days.
ReserveFee.amount() is five lira for each of the hours.
IPython console
In [1]: %run untitled0.py
The second line is the whole question: the text came from the parent's printed form and the number came from the child's amount, because the parent's method asked self.amount() and self is a ReserveFee.
Hint 1/4
For the second print, ask two separate questions: which class supplies the printed form, and which class supplies the amount it reports.
Hint 2/4
A method the subclass does not define is answered by the parent. An inherited method that calls self.something() reaches the subclass version when there is one.
Hint 3/4
Here ReserveFee has no printed form, so Fee.__repr__ runs, and it calls self.amount() on an object whose own class does define amount, with 3 days and 4 hours in it.
Hint 4/4
The first line uses the days at two lira, the second uses the hours at five lira, and the third reads the day count through the inherited get method.
Show solution
Answer the printed form and the amount as two separate lookups.
Independent check: the second number is not a multiple of 2, so it cannot have come from the parent's amount, and the third line proves the day count was stored all along.
⚠ A subclass __init__ with no call to the parent's
The child's own fields are set and the program runs, so nothing looks wrong until an inherited method reads one of the parent's slots.
Every exercise this week begins with a list of data members, a list of methods and a sample run.
List the data members and decide where each lives
One private instance variable per data member, set in __init__. Anything the specification describes as the same for all objects, a fee or a capacity or a rate, is a class variable instead, written under the class line.
Write __init__ with one parameter per value passed in
Take the parameters in the order the specification names them, and give a default to any it says is optional.
Write one get method per readable member
Three words each: def get_x(self): return self.__x. Write a set method only where the specification asks for one, and put its condition inside it.
Write the printed form against the sample run
Copy the labels out of the sample run character for character, including the colons and the spaces.
Where it goes wrong
Writing the script before the class and then bending the class to fit the calls already written.
One get method that returns several fields at once. The specification asks for one per member, and a single method returning a tuple then has to be unpacked at every call.
Guessing the printed form instead of copying it from the sample run. Marks here are given for a match, and a missing colon is a mismatch.
Reading a specification for the special method it is asking for
When a sentence in the specification talks about two objects rather than one, or about what a built in operation should do with your objects.
Find the sentence that mentions two objects
Phrases to look for: is less than, comes before, is sorted by, counts as the same as, is equal to, added to.
Translate the phrase into the method name
Sorted by or comes before or is less than means __lt__. Counts as the same or is equal to means __eq__.
Count the fields the sentence mentions
One field is one test. Two fields with an if their values are equal between them is two tests, and the second is guarded by an equality on the first.
Decide what the method hands back
A comparison returns True or False. An arithmetic method returns a new object of the class and changes neither side.
Where it goes wrong
Writing a method with a descriptive name, such as is_cheaper, when the specification also says the list has to be sorted.
Reading is sorted by count, and if counts are equal by name as one test joined with or, which sorts by name whenever the name happens to compare the other way.
Making __add__ change the object on the left. Then c = a + b quietly damages a, which nothing about the plus sign warns you of.
Tracing a program that builds objects
Any question that shows a class and a few lines using it and asks what it prints.
Draw one box per object and one shelf for the class
Every call of the class name is a new box. Values assigned under the class line, outside any method, go on the class shelf and not in any box.
Run __init__ once per box, filling its slots
Take the arguments in order, remembering that self is filled in for you.
For each call, find the method before you run it
Start at the class of the object the call was made on and walk up the parents until you find a method of that name.
Run the body against the box, not against the class
self.x reads the box the call reached. Book.x reads the class shelf. An assignment to self.x writes the box, even when a value of that name is sitting on the shelf, and that is the one line worth slowing down for.
Where it goes wrong
Reading the class top to bottom and then the script, rather than following the script and visiting the class as it calls.
Keeping one set of values in your head for the whole program when there are two objects.
Assuming that because a method changed something it also returned it, which produces an output line with a value where None belongs.
A subclass __init__ that calls the parent's
Three lines in the child's __init__: the super call, then its own field. Both an inherited method and a new one are then asked for.
classBook(object):
"""A catalogue entry with a title and a page count."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.__title = title
self.__pages = pages
defget_title(self):
"""Returns the title."""returnself.__title
classAudioBook(Book):
"""A catalogue entry that lets Book set the shared data."""def__init__(self, title, pages, minutes):
"""Assumes minutes is an int. The title and the page count are set by the Book __init__."""super().__init__(title, pages)
self.__minutes = minutes
defget_minutes(self):
"""Returns how many minutes of audio there are."""returnself.__minutes
a = AudioBook('Zeno', 98, 205)
print(a.get_minutes())
print(a.get_title())
Sample Run:
205
Zeno
FindThe two printed lines.
Given
Book.__init__ sets a private title and a private page count.
AudioBook.__init__ calls it and then sets its own minutes.
Solution
Let the parent fill its own slots
$$\texttt{super().\_\_init\_\_(title, pages)}$$
The only line that can create _Book__title, because only code written inside Book spells that name.
$$\texttt{self.\_\_minutes = minutes}$$
Afterwards, and this one belongs to the child.
Both methods now work
$$\texttt{a.get\_minutes()}\ \to\ 205$$
The child's own field through the child's own method.
$$\texttt{a.get\_title()}\ \to\ \texttt{Zeno}$$
An inherited method reading a slot the super call created.
Answer $$\boxed{205\ /\ \texttt{Zeno}}$$
Check
Independent check: isinstance(a, Book) is True in both versions of this class, so being a Book is not what makes the title work.
The same subclass with the super call left out
One line removed. The object is built without complaint, the child's own method works, and the program stops on the line after.
classBook(object):
"""A catalogue entry with a title and a page count."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.__title = title
self.__pages = pages
defget_title(self):
"""Returns the title."""returnself.__title
classAudioBook(Book):
"""A catalogue entry that sets only its own data."""def__init__(self, title, pages, minutes):
"""Assumes minutes is an int. Sets only the audio length."""self.__minutes = minutes
defget_minutes(self):
"""Returns how many minutes of audio there are."""returnself.__minutes
a = AudioBook('Zeno', 98, 205)
print(a.get_minutes())
print(a.get_title())
Sample Run:
205
Traceback (most recent call last):
File "audio.py", line 28, in <module>
print(a.get_title())
^^^^^^^^^^^^^
File "audio.py", line 11, in get_title
returnself.__title
^^^^^^^^^^^^
AttributeError: 'AudioBook'object has no attribute '_Book__title'
FindWhere the program stops and which slot is missing.
Given
AudioBook.__init__ sets only its own minutes.
The same two calls are made as in the version above.
Solution
Nothing complains at construction
$$\texttt{AudioBook('Zeno', 98, 205)}$$
Two of the three arguments are accepted and never used.
$$\texttt{a.get\_minutes()}\ \to\ 205$$
The child's own field is fine, which is why the fault is easy to miss in testing.
The message names the parent's spelling, which is the clue that the missing thing is the super call and not the get method.
Answer $$\boxed{205,\ \text{then an AttributeError on}\ \texttt{\_Book\_\_title}}$$
Check
Independent check: the traceback has two frames, the call to get_title and the line inside it.
The two classes differ by one line, both build an object without complaint, and the difference only appears when an inherited method reads a slot of the parent.
How to tell them apart
If a subclass writes its own __init__, its first line calls the parent's. The test is not whether the object exists but whether every inherited get method returns something, so call one of them while testing.
Scaffolding comes off
The common skeleton
Name the data members from the specification and sort them into two groups: per object, which are set in __init__, and shared by the class, which are written under the class line.
Write __init__ with one parameter for each value the caller passes. Give a slot a safe starting value before calling a set method on it, so that the condition can refuse the first value without leaving the slot missing.
Write one get method per readable member, and a set method only where the specification asks, with its condition inside the set method.
Write the behaviour methods. A method that reports returns a value; a method that updates changes a slot and returns nothing. Read a shared value through the class name.
Write the printed form last, copying the labels from the sample run and converting every number with str or format.
Build two objects, not one, and call every method on both, including one call the set method should refuse.
1 · fully worked
A Locker class, written from its specification
The specification, in the form the lab sheets use. A dormitory locker has a number, the name of the student holding it, and a monthly rent.
classLocker(object):
"""A dormitory locker rented for a term."""
__deposit = 250.0def__init__(self, number, holder, monthly):
"""Assumes number is an int, holder a str, monthly a float."""self.__number = number
self.__holder = holder
self.__monthly = 0.0self.set_monthly(monthly)
defget_number(self):
"""Returns the locker number."""returnself.__number
defget_holder(self):
"""Returns the name of the student renting it."""returnself.__holder
defget_monthly(self):
"""Returns the monthly rent."""returnself.__monthly
defset_monthly(self, monthly):
"""Assumes monthly is a number. Sets the rent only when the value is positive."""if monthly > 0:
self.__monthly = monthly
defget_deposit(self):
"""Returns the deposit every locker shares."""return Locker.__deposit
defterm_cost(self, months):
"""Assumes months is an int >= 0. Returns the rent for that many months plus the deposit."""returnself.__monthly * months + Locker.__deposit
def__repr__(self):
"""Returns the four line block the office prints."""return ('Locker: ' + str(self.__number) + '\n'
+ 'Holder: ' + self.__holder + '\n'
+ 'Monthly: ' + format(self.__monthly, '.2f') + '\n'
+ 'Term cost: ' + format(self.term_cost(4), '.2f') + '\n')
first = Locker(114, 'Elif Karaman', 120.0)
second = Locker(207, 'Bora Aral', 95.5)
second.set_monthly(-10.0)
print(first)
print(second)
print('deposit shared by both:', first.get_deposit() == second.get_deposit())
Sample Run:
Locker: 114
Holder: Elif Karaman
Monthly: 120.00
Term cost: 730.00
Locker: 207
Holder: Bora Aral
Monthly: 95.50
Term cost: 632.00
deposit shared by both: True
FindThe two blocks and the line that compares the two deposits.
Given
Locker 114 is held by Elif Karaman at 120.0 a month.
Locker 207 is held by Bora Aral at 95.5 a month, and then set_monthly(-10.0) is called on it.
The printed block shows the cost of a four month term.
Solution
Sort the members into per object and shared
$$\texttt{\_\_deposit = 250.0}\ \text{under the class line}$$
The specification says every locker shares it, so one slot for the class.
Three per object slots, because each locker has its own number, holder and rent.
Write the init so the condition covers the first value
$$\texttt{self.\_\_monthly = 0.0}$$
A safe value first. Without it, a first rent that failed the condition would leave the object with no rent slot, and the next get would raise an AttributeError rather than return something.
$$\texttt{self.set\_monthly(monthly)}$$
The specification says to initialise through the set method, which is what makes the condition apply to the caller's value too.
Money to two decimals, so 120.0 prints as 120.00 and matches.
$$\texttt{self.term\_cost(4)}\ \text{inside the form}$$
A printed form is allowed to call another method of the same object, which keeps the four in one place.
Answer $$\boxed{\text{730.00 and 632.00, and the deposits compare equal}}$$
Check
Independent check on the two term costs: they differ by 24.5 times 4, which is 98.0, and 730.00 minus 632.00 is 98.00.
Thirty lines of class for a specification of six sentences. Four of those lines are the get methods and five are the printed form.
Written in this order, the class comes out in one pass and the only thing left to check is the printed form against the sample run.
2 · you write the reasoning
Same skeleton, smaller specification: a tally has a label and a count that starts at zero, a get method for the count, a method that adds a number to it, and a printed form of one line.
classTally(object):
"""Counts one kind of thing under a label."""def__init__(self, label):
"""Assumes label is a str. The count starts at zero."""self.__label = label
self.__count = 0defget_count(self):
"""Returns the count so far."""returnself.__count
defadd(self, howmany):
"""Assumes howmany is an int >= 0. Adds it to the count."""self.__count = self.__count + howmany
def__repr__(self):
"""Returns the label and the count on one line."""returnself.__label + ': ' + str(self.__count)
gate = Tally('entrance')
gate.add(4)
gate.add(3)
print(gate)
print(gate.get_count())
Sample Run:
entrance: 77
The count starts at zero inside __init__ rather than under the class line.
reasoning
Because each tally counts its own things. Under the class line there would be one count for the whole program, and the second tally would start from whatever the first had reached.
__init__ takes the label but not the count.
reasoning
Because the specification says the count starts at zero, so it is not a value the caller supplies. A parameter with a default of zero would also work and would let a caller start a tally part way through, which the specification does not ask for.
add writes back to the same slot it read.
reasoning
Because the new value depends on the old one, which is what makes this an update rather than a set. The slot on the left and the read on the right are the same slot, and the order of the two is what carries the count forward.
add has no return and the two calls stand on lines of their own.
reasoning
Because it is in the changing family: it changes the object and hands back None. Assigning from it would store None, and the next line that used that name as a number would stop the program.
The printed form wraps the count in str and the label in nothing.
reasoning
Because the count is an int and a plus sign with text on one side refuses an int on the other. The label is already text, so converting it would be work with no effect.
3 · find the buried error
Harder, and now the program is somebody else's. A cinema ticket has a film, a seat and a price, and every ticket shares one booking fee that starts at 5.0.
classTicket(object):
"""One cinema ticket. Every ticket shares the same booking fee."""
__fee = 5.0def__init__(self, film, seat, price):
"""Assumes film and seat are str, price a float."""self.__film = film
self.__seat = seat
self.__price = price
deftotal(self):
"""Returns the price with the booking fee added."""returnself.__price + Ticket.__fee
defraise_fee(self, amount):
"""Assumes amount is a number. Raises the booking fee that every ticket shares."""self.__fee = Ticket.__fee + amount
def__lt__(self, other):
"""Assumes other is a Ticket. The cheaper ticket comes first."""return other.__price < self.__price
def__repr__(self):
"""Returns the film, the seat and the total on one line."""return (self.__film + ' ' + self.__seat + ' '
+ format(self.total(), '.2f'))
box = [Ticket('Dune', 'C7', 90.0), Ticket('Alien', 'A1', 60.0),
Ticket('Solaris', 'B4', 75.0)]
box[0].raise_fee(3.0)
box.sort()
print(box)
Sample Run:
[Dune C7 95.00, Solaris B4 80.00, Alien A1 65.00]
Every total is 3.00 too low and the order is the wrong way round. Two lines are at fault and the rest of the class is right.
Put the booking fee where the specification puts it: one value for every ticket, written under the class line.
Store the three fields of a ticket privately in __init__.
The total is the price of this ticket plus the shared fee, read through the class name.
Raising the fee adds the amount to the shared value.
Cheapest first, so a ticket is less than another when its price is the lower one.
The printed form shows the film, the seat and the total to two decimals.
the two buried errors (2)
⚠ step 4
The left hand side is self.__fee, which builds a private slot inside the one ticket raise_fee was called on and leaves the shared value at 5.0.
Inside a method, self is the object and the object is a Ticket, so the two spellings look interchangeable.
right
Write the class name on the left: Ticket.__fee = Ticket.__fee + amount. Then every ticket, including the ones never touched, reports the new fee.
⚠ step 5
The two sides are the wrong way round. other.__price < self.__price is True when this ticket is the more expensive one, so sort puts the most expensive first and the box prints Dune, Solaris, Alien instead of Alien, Solaris, Dune.
The line mentions both prices and the words cheapest first are in the docstring, so it reads correctly at a glance.
right
Write the object the method was called on first: return self.__price < other.__price. With a tie breaker the same rule holds, self on the left in both tests.
4 · the bare problem
§08.3 — a Court class from a bare specification
Same skeleton, no scaffolding. Write the class and the short script, then check your printed form against the sample run below character for character.
Court: 3
Surface: clay
Two hours off peak: 120.00
Court: 7
Surface: hard
Two hours off peak: 170.00
peak charge shared: True
clay two hours at peak: 160.00
Find
(a) Write the class Court.
(b) Write the script that produces the sample run above.
Given
Court 3 is clay at 60.0 an hour and court 7 is hard at 85.0.
The peak charge is 40.0 and is shared by every court.
Hint 1/4
Sort the four things the specification names into two groups before writing any code: which of them belongs to one court and which belongs to the class.
Hint 2/4
One private instance variable per data member set in __init__, one class variable under the class line for the shared charge, one get method per member, the condition inside the set method, and a printed form copied from the sample run.
Hint 3/4
Here the shared value is the peak charge of 40.0, the per court values are the number, the surface and the hourly rate, and the sample run labels are Court: , Surface: and Two hours off peak: with the cost to two decimals.
Hint 4/4
The two printed blocks show 120.00 and 170.00, the rate of -20.0 is refused so the hard court stays at 85.0, and two peak hours on clay come to 160.00.
Show solution
Write the class from the specification in the order of the skeleton and leave the printed form until last.
Sort the members
$$\texttt{\_\_peak\_extra = 40.0}\ \text{under the class line}$$
Independent check on the refusal and the peak charge together: 170.00 is twice 85.0, which proves the -20.0 was refused, and 160.00 minus 120.00 is 40.00, which proves the peak charge was added once.
Every specification in this week's labs is this shape. The only parts that change are the names and how many fields there are.
Full exam-style question
A Delivery class and the script that loads, sorts and prices a vanexam format
Exam shape, in two parts, of the kind a past final paper used: a class worth the smaller half of the marks and a script worth the larger half.
Part one. A parcel has a code, a destination city and a weight in kilograms.
# The data file comes with the lab. It is written here first so that this# program can be run on its own.
out = open('parcels.txt', 'w')
out.write('TR104,Ankara,3.5\n')
out.write('TR087,Izmir,12.0\n')
out.write('TR221,Bursa,3.5\n')
out.write('TR005,Adana,7.25\n')
out.close()
classDelivery(object):
"""One parcel waiting to go out. Every parcel shares one rate."""
__rate = 12.5def__init__(self, code, city, weight):
"""Assumes code and city are str, weight a float."""self.__code = code
self.__city = city
self.__weight = 0.0self.set_weight(weight)
defget_code(self):
"""Returns the parcel code."""returnself.__code
defget_city(self):
"""Returns the destination city."""returnself.__city
defget_weight(self):
"""Returns the weight in kilograms."""returnself.__weight
defset_weight(self, weight):
"""Assumes weight is a number. Sets the weight only when the value is positive."""if weight > 0:
self.__weight = weight
defget_rate(self):
"""Returns the rate per kilogram that every parcel shares."""return Delivery.__rate
defcost(self):
"""Returns the price of sending this parcel."""returnself.__weight * Delivery.__rate
def__lt__(self, other):
"""Assumes other is a Delivery. Returns True when this parcel is lighter, and when the two weights are equal, when this code comes first in alphabetical order."""ifself.__weight < other.__weight:
returnTrueifself.__weight == other.__weight andself.__code < other.__code:
returnTruereturnFalsedef__repr__(self):
"""Returns the three line block the desk prints."""return ('Code: ' + self.__code + '\n'
+ 'City: ' + self.__city + '\n'
+ 'Cost: ' + format(self.cost(), '.2f') + '\n')
defload_parcels(filename):
"""Assumes filename names a file of code, city and weight lines. Returns a list of Delivery objects built from that file."""
parcels = []
in_file = open(filename, 'r')
for line in in_file:
field = line.strip().split(',')
parcels.append(Delivery(field[0], field[1], float(field[2])))
in_file.close()
return parcels
deftotal_cost(parcels):
"""Assumes parcels is a list of Delivery objects. Returns what the whole list costs to send."""
total = 0.0for p in parcels:
total = total + p.cost()
return total
van = load_parcels('parcels.txt')
van.sort()
print('Parcels lightest first:')
print(van)
print('Total cost:', format(total_cost(van), '.2f'))
print('Heaviest parcel goes to', van[len(van) - 1].get_city())
Sample Run:
Parcels lightest first:
[Code: TR104
City: Ankara
Cost: 43.75
, Code: TR221
City: Bursa
Cost: 43.75
, Code: TR005
City: Adana
Cost: 90.62
, Code: TR087
City: Izmir
Cost: 150.00
]
Total cost: 328.12
Heaviest parcel goes to Izmir
FindThe sorted list, the total cost, and the destination of the heaviest parcel.
Given
The file holds TR104 to Ankara at 3.5, TR087 to Izmir at 12.0, TR221 to Bursa at 3.5, and TR005 to Adana at 7.25.
The rate is 12.5 a kilogram and is shared by every parcel.
Two of the four parcels weigh the same, which is what the tie breaker is for.
Solution
Part one: the members, and where each lives
$$\texttt{\_\_rate = 12.5}\ \text{under the class line}$$
Shared by every parcel, so one slot. Reading it through Delivery.__rate inside cost says where it lives.
The main field. Lighter first, as the specification says.
$$\texttt{if ... == ... and self.\_\_code < other.\_\_code}$$
The tie breaker, guarded by the equality so that it cannot overrule the weight.
Part two: one object per line
$$\texttt{field = line.strip().split(',')}$$
The strip comes first, because the newline would otherwise end up inside the last field and float would still accept it while a city name would silently carry it.
Two independent checks. The order: the weights in the printed order are 3.5, 3.5, 7.25 and 12.0, which is increasing, and the tied pair is in code order.
About fifty lines for a question of two paragraphs. Thirty of them are the class, and twenty of those thirty are get methods and the printed form, which is why the class is worth writing first and fast.
The two halves of an exam question like this are separable: the class can be written and tested from two objects built by hand, before the file is read at all.
Practice
A · concept 3 questions
1§08.3 — what two underscores actually promise
A class keeps its price with two leading underscores and offers a set method that refuses a negative value.
classBook(object):
"""A book whose hidden name we are about to read out loud."""def__init__(self, title, price):
"""Assumes title is a str and price a float."""self.__title = title
self.__price = price
defget_price(self):
"""Returns the price."""returnself.__price
b = Book('Ada', 180.0)
print(b.get_price())
b._Book__price = -99.0print(b.get_price())
Sample Run:
180.0
-99.0
The claim: no line outside the class can put a negative number into the price, because the attribute is private.
Find(a) True or false, with the reason.
Given
The attribute is written self.__price inside class Book.
The program contains one line that is not a method call and does change it.
Hint 1/4
Ask what Python does to the name rather than what the word private suggests.
Hint 2/4
Two leading underscores inside a class body are rewritten with the class name in front, so __price inside class Book is stored as _Book__price.
Hint 3/4
Here the middle line writes b._Book__price = -99.0, which is the stored spelling, and the get method then reports it.
Hint 4/4
False: the price becomes -99.0 without any set method being called.
Show solution
Test the claim against the output first and only then explain it.
The rewriting happens wherever the short name is written inside class Book, which is what makes the long name the real one.
Ask what the offending line spells
$$\texttt{b.\_Book\_\_price = -99.0}$$
The long name, written from outside, where no rewriting happens.
$$\texttt{b.get\_price()}\ \to\ -99.0$$
The get method reads the same slot, which is how the change becomes visible.
Answer $$\boxed{\text{False}}$$
Check
Independent check: b.__price = -99.0 written instead, with the short name, does not change the price at all.
2§08.7 — a subclass and the parent's private data
A subclass needs the page count that the parent keeps private, and writes self.__pages inside one of its own methods.
classBook(object):
"""A catalogue entry whose page count is hidden."""def__init__(self, title, pages):
"""Assumes title is a str and pages an int."""self.__title = title
self.__pages = pages
defget_pages(self):
"""Returns the page count."""returnself.__pages
classAudioBook(Book):
"""A catalogue entry that reaches for the hidden page count."""def__init__(self, title, pages, minutes):
"""Assumes minutes is an int."""super().__init__(title, pages)
self.__minutes = minutes
defpages_per_minute(self):
"""Returns how many pages one minute of audio covers."""returnself.__pages / self.__minutes
a = AudioBook('Zeno', 98, 205)
print(a.get_pages())
print(a.pages_per_minute())
Sample Run:
98
Traceback (most recent call last):
File "audio.py", line 29, in <module>
print(a.pages_per_minute())
^^^^^^^^^^^^^^^^^^^^
File "audio.py", line 24, in pages_per_minute
returnself.__pages / self.__minutes
^^^^^^^^^^^^
AttributeError: 'AudioBook'object has no attribute '_AudioBook__pages'. Did you mean: '_AudioBook__minutes'?
The claim: the line works, because an AudioBook inherits the page count and self is the object that holds it.
Find(a) True or false, with the reason.
Given
Book.__init__ sets self.__pages and Book.get_pages returns it.
AudioBook.pages_per_minute writes self.__pages.
Hint 1/4
Separate two questions that feel like one: does the object hold the page count, and can this line spell the name of the slot it is in.
Hint 2/4
Two leading underscores are rewritten with the name of the class the line is written in, not the class the slot came from.
Hint 3/4
Here the slot was created by Book, so it is _Book__pages, and the failing line is written in AudioBook, so it asks for _AudioBook__pages.
Hint 4/4
False: the program stops with an AttributeError, and the fix is the inherited get method.
Show solution
Use the line that works as evidence about the line that does not.
Establish that the data is there
$$\texttt{a.get\_pages()}\ \to\ 98$$
An inherited method, and it finds the slot, so nothing is missing from the object.
Rewritten to _AudioBook__pages, because the rewriting uses the class the line is written in.
$$\texttt{\_Book\_\_pages}\ \text{is what exists}$$
Created by Book's __init__, which is where that spelling comes from.
Answer $$\boxed{\text{False}}$$
Check
Independent check: the message ends with a suggestion naming _AudioBook__minutes, which is the nearest attribute the object really has.
3§08.4 — writing a shared value through self
A class keeps one fine per day for every book and offers a method meant to raise it.
classBook(object):
"""A book whose method writes the shared fine through self."""
__fine_per_day = 2.5def__init__(self, title):
"""Assumes title is a str."""self.__title = title
defraise_fine(self, amount):
"""Assumes amount is a number. Meant to raise the shared fine."""self.__fine_per_day = Book.__fine_per_day + amount
deffine(self, days_late):
"""Assumes days_late is an int >= 0. Returns the fine owed."""return days_late * Book.__fine_per_day
b1 = Book('Ada')
b2 = Book('Zeno')
b1.raise_fine(1.5)
print(b1.fine(2), b2.fine(2))
print(b1._Book__fine_per_day, Book._Book__fine_per_day)
Sample Run:
5.05.04.02.5
Decide which line below describes what that method did.
Find(a) Which of the four descriptions is what happened?
Given
__fine_per_day is 2.5 and is written under the class line.
raise_fine assigns to self.__fine_per_day.
Hint 1/4
Read the last line of the program before deciding anything. It prints two values that the rest of the program keeps apart, and their being different is the answer.
Hint 2/4
A read through self looks in the object and then in the class, so it finds a shared value.
Hint 3/4
Here the write is self.__fine_per_day = Book.__fine_per_day + amount, and the method that reports a fine reads Book.__fine_per_day.
Hint 4/4
Both fines are still 5.0, and the last line shows 4.0 in the object beside 2.5 on the class.
Show solution
Let the last line of the program settle it rather than reasoning about what the method meant.
Read the two fines
$$\texttt{5.0 5.0}$$
Two days at 2.5. Nothing the reporting method can see changed, so neither description that claims a higher fine can be right.
The shared slot is untouched. One object with its own value, one class with the old one.
Answer $$\boxed{\text{a private slot in one object, shared value unchanged}}$$
Check
Independent check: the same program with Book on the left of the assignment prints 8.0 8.0, so the difference between the two versions is 3.0 in every fine and is visible on the first line.
B · computation 5 questions
1§08.1 — an object in a list and a name for the same object
Two lamps. One of them has a name of its own and is also an item of a list, and the other is reached first through the list and then through a new name.
classLamp(object):
"""A desk lamp with a brightness level."""def__init__(self, place, level):
"""Assumes place is a str and level an int."""self.place = place
self.level = level
desk = Lamp('desk', 3)
row = [desk, Lamp('shelf', 1)]
row[0].level = 5print(desk.level)
brighter = row[1]
brighter.level = brighter.level + 2print(row[1].level)
print(desk is row[0], brighter is row[1])
Find(a) Write the three lines this prints.
Given
desk is built with level 3 and the second lamp with level 1.
row holds the desk lamp and the shelf lamp, in that order.
IPython console
In [1]: %run untitled0.py
Two objects and four ways of reaching them. The change written through row[0] shows up in desk, and the change written through brighter shows up in row[1], because in each case there is one object with two references to it.
Hint 1/4
Count the objects, then count the names and the list slots pointing at them.
Hint 2/4
A name and a list slot can hold a reference to the same object, and a change made through either is visible through both, because there is one object.
Hint 3/4
Here row[0] and desk are the same object, and brighter and row[1] are the same object, so the two changes land on the two lamps.
Hint 4/4
The first line is 5, the second is 3, and the third reports True twice.
Show solution
Draw two boxes and four arrows before reading the changes.
Draw the references
$$\texttt{desk}\ \text{and}\ \texttt{row[0]}$$
One object, two references, because the list was built with the name desk in it.
One object, two references, because the assignment copied a reference out of the list.
Apply each change once
$$\texttt{row[0].level = 5}$$
Reaches the object desk also names, so the first print is 5.
$$\texttt{brighter.level = 1 + 2 = 3}$$
Reaches the object row[1] also names, so the second print is 3.
Answer $$\boxed{5\ /\ 3\ /\ \texttt{True True}}$$
Check
Independent check: len(row) is 2 throughout, so no third object was built anywhere, which is what the two True answers are reporting.
2§08.5 — a class with only the friendly printed form
A luggage tag with a __str__ and no __repr__. The last line tests the exact form instead of printing it, because the real text ends in an address that changes on every run.
classTag(object):
"""A luggage tag with a code on it."""def__init__(self, code):
"""Assumes code is a str."""self.__code = code
defget_code(self):
"""Returns the code."""returnself.__code
def__str__(self):
"""Returns the code in square brackets."""return'[' + self.__code + ']'
t = Tag('AB12')
print(t)
print(str(t))
opening = repr(t)[:26]
print(opening == '<__main__.Tag object at 0x')
Find(a) Write the three lines this prints.
Given
__str__ returns the code in square brackets.
There is no __repr__.
IPython console
In [1]: %run untitled0.py
The fallback only works in one direction. A missing __str__ falls back to __repr__, and a missing __repr__ falls back to the default, never to __str__.
Hint 1/4
Decide, for each of the three lines, which of the two printed forms it is asking for.
Hint 2/4
print(x) and str(x) ask for __str__ first. repr(x) asks for __repr__ and does not fall back to __str__, so a class without one gets the default.
Hint 3/4
Here __str__ is defined and returns [AB12], and __repr__ is not defined, so the first 26 characters of repr(t) are <__main__.Tag object at 0x.
Hint 4/4
The first two lines are the same and the third is True.
Show solution
Answer the third line first. It is the only one that involves the missing method, so once it is settled the other two are a single lookup.
Independent check: add a __repr__ returning the code with no brackets and the third line becomes False while the first two stay the same, which shows which line the missing method was affecting.
3§08.4 — a class variable used as a counter
A queue ticket machine. The class counts how many tickets it has handed out, and each ticket remembers its own number.
classQueue(object):
"""A queue ticket. The class counts how many it has handed out."""
__issued = 0def__init__(self, name):
"""Assumes name is a str. Counts this ticket as it is made."""self.__name = name
Queue.__issued = Queue.__issued + 1self.__number = Queue.__issued
defget_number(self):
"""Returns the number printed on this ticket."""returnself.__number
defget_issued(self):
"""Returns how many tickets have been handed out in total."""return Queue.__issued
first = Queue('Elif')
second = Queue('Bora')
third = Queue('Deniz')
print(first.get_number(), second.get_number(), third.get_number())
print(first.get_issued(), third.get_issued())
Find(a) Write the two lines this prints.
Given
__issued starts at 0 and is written under the class line.
__init__ raises it by one and then copies it into self.__number.
IPython console
In [1]: %run untitled0.py
The copy is what makes this work. The shared count keeps rising, and each ticket keeps the value the count had when it was built, so the numbers are 1, 2 and 3 and stay that way.
Hint 1/4
Notice that two different things are being kept here and that one line of __init__ turns one into the other.
Hint 2/4
A name assigned under the class line is one slot for the class, and an assignment through the class name updates it for everybody.
Hint 3/4
Here Queue.__issued = Queue.__issued + 1 runs once per ticket, and self.__number = Queue.__issued copies the shared total into the ticket at the moment it was built.
Hint 4/4
The three numbers are 1, 2 and 3, and both tickets report the same total of 3.
Show solution
Keep two columns as you read: one for the shared count and one for each ticket's own number.
Independent check: the last number printed on the first line equals both numbers on the second, which has to be true if the third ticket was the last one built.
4§08.6 — sorted, sort, min and the greater than sign
Three storage boxes with a comparison method on volume. Four prints: the two ways of ordering, then the smallest, then a comparison written with the sign the class never defined.
classBox(object):
"""A storage box with a volume in litres."""def__init__(self, label, litres):
"""Assumes label is a str and litres an int."""self.__label = label
self.__litres = litres
def__lt__(self, other):
"""Assumes other is a Box. True when this box is the smaller one."""returnself.__litres < other.__litres
def__repr__(self):
"""Returns the label and the volume on one line."""returnself.__label + ':' + str(self.__litres)
store = [Box('red', 40), Box('blue', 15), Box('green', 60)]
print(sorted(store))
print(store)
print(min(store))
print(store[0] > store[1])
Find(a) Write the four lines this prints.
Given
The boxes are red 40, blue 15 and green 60, in that order.
__lt__ compares the volumes and there is no __gt__.
IPython console
In [1]: %run untitled0.py
The second line is the one that catches people: sorted was called and the list is still in the order it was written, because that function builds a new list.
Hint 1/4
Two of the four lines are about the list and two are about a single decision.
Hint 2/4
sorted(L) builds a new list and leaves the original alone, while L.sort() would change it.
Hint 3/4
Here the only call made is sorted(store), so the list itself is never rearranged, and the two boxes compared on the last line are red at 40 and blue at 15.
Hint 4/4
The first line is in order, the second is as written, the third is the blue box, and the fourth is True.
Show solution
Deal with the list lines before the comparison lines. Doing it the other way invites you to carry an order that was never applied.
Independent check: the two list lines hold the same three boxes in different orders, which can only happen if one of them is a new list.
5§08.6 — equality decided in lower case
A word that counts as equal to another when the letters match whatever the case. Four prints, three of which are list operations that go through the comparison.
classWord(object):
"""A word that counts as equal when the letters match, whatever the case."""def__init__(self, text):
"""Assumes text is a str."""self.__text = text
defget_text(self):
"""Returns the word as it was given."""returnself.__text
def__eq__(self, other):
"""Assumes other is a Word. Compares the two in lower case."""returnself.__text.lower() == other.__text.lower()
def__repr__(self):
"""Returns the word as it was given."""returnself.__text
bag = [Word('Ada'), Word('Zeno'), Word('milo')]
print(Word('ADA') in bag)
print(bag.index(Word('MILO')))
print(Word('Ada') == Word('ada'), Word('Ada') is Word('Ada'))
print(bag.count(Word('zeno')))
Find(a) Write the four lines this prints.
Given
The bag holds Ada, Zeno and milo, written with those capitals.
__eq__ compares the two words in lower case.
IPython console
In [1]: %run untitled0.py
One method, four answers. Writing __eq__ changed what in, index and count mean for this class, which is more places than the method name suggests.
Hint 1/4
Three of these four lines ask the list something and one asks two objects directly.
Hint 2/4
in, index and count all walk the list comparing with ==, which is __eq__.
Hint 3/4
Here __eq__ lowers both sides before comparing, and the bag holds Ada at position 0, Zeno at 1 and milo at 2.
Hint 4/4
The first line is True, the second is 2, the third reports True and then False, and the last is 1.
Show solution
Settle what the comparison does once, then apply it to each of the three list operations.
Independent check: remove __eq__ from the class and the first line becomes False, the second stops the program because the item is not found, and the last becomes 0, while the is answer does not move.
C · exam level 3 questions
1§08.7 — four classes, four calls, which method each reaches
Four classes in two branches, two of which define the method and two of which do not. This is the shape a past final paper used for its inheritance part.
classPen(object):
"""The base price rule for anything you write with."""defcost(self, n):
"""Assumes n is an int. Returns the cost of n items."""return n * 3classMarker(Pen):
"""Priced exactly like a pen, so nothing is added."""passclassPencil(Pen):
"""Priced by the item with a fixed box charge on top."""defcost(self, n, extra=10):
"""Assumes n is an int. Returns the cost of n items plus the box."""return n + extra
classCrayon(Marker):
"""Priced like a marker, one lira off."""defcost(self, n):
"""Assumes n is an int. Returns the cost of n items, less one lira."""return n - 1print(Pen().cost(4))
print(Marker().cost(6))
print(Pencil().cost(3))
print(Crayon().cost(9))
Find(a) Write the four numbers this prints.
Given
Pen.cost(n) returns three times n.
Marker(Pen) has pass as its body.
IPython console
In [1]: %run untitled0.py
Marker has pass, so its call walks up to Pen and multiplies: 6 times 3 is 18. Crayon defines its own, so its call stops at Crayon and never reaches Marker or Pen, and the fact that Marker has nothing to offer makes no difference.
Hint 1/4
Draw the family tree before you read any of the four calls. Two of the classes hang off one parent and one hangs off a child.
Hint 2/4
The search for a method starts at the class of the object and walks up the parents until it finds a match.
Hint 3/4
Here Pen and Pencil and Crayon each define the method, Marker does not, and the calls are made on one object of each class with the arguments 4, 6, 3 and 9.
Hint 4/4
Two of the answers use the multiplication, one uses the addition with the default 10, and one subtracts a lira.
Show solution
Draw the tree first and answer each call in a single step.
A chain of three. Crayon's parent is Marker, whose parent is Pen.
$$\texttt{Pen}\ \to\ \texttt{Pencil}$$
A separate branch of two, with no connection to Marker or Crayon.
One step per call
$$\texttt{Pen().cost(4)} = 4 \times 3 = 12$$
Found in Pen.
$$\texttt{Marker().cost(6)} = 6 \times 3 = 18$$
Not found in Marker, so one step up to Pen.
Answer $$\boxed{12,\ 18,\ 13,\ 8}$$
Check
Independent check by shape rather than by value: 12 and 18 are multiples of 3 and the other two are not, which tells you at a glance which calls reached Pen.
2§08.3 — a shelf that never accepts a book
A shelf is supposed to hold books up to its capacity and refuse the rest. Three books are offered to a shelf of capacity 2, so it should report 2 at the end.
classShelf(object):
"""A shelf that refuses to hold more than its capacity."""def__init__(self, code, capacity):
"""Assumes code is a str and capacity an int > 0."""self.__code = code
self.__capacity = capacity
self.__books = []
defget_count(self):
"""Returns how many books are on the shelf."""returnlen(self.__books)
defadd(self, title):
"""Assumes title is a str. Adds the book only while there is still room."""ifself.get_count() > self.__capacity:
self.__books.append(title)
def__repr__(self):
"""Returns the code and the count on one line."""returnself.__code + ' holds ' + str(self.get_count())
s = Shelf('A1', 2)
s.add('Ada')
s.add('Zeno')
s.add('Milo')
print(s)
print(s.get_count())
Sample Run:
A1 holds 00
Below is the reasoning the author wrote out. One of the five steps is wrong.
Find
(a) Which step is wrong?
(b) What should that line say instead?
Given
The shelf is built with capacity 2 and an empty list.
Three titles are offered: Ada, Zeno and Milo.
Hint 1/4
Ask what the reported number would have to be if the guarded line never ran at all, and compare that with what the program actually reports.
Hint 2/4
A condition on a growing collection has to be true while there is still room, which means comparing the current count with the capacity in the direction that is true at the start.
Hint 3/4
Here the shelf starts with zero books and a capacity of 2, and the condition written is that the count is greater than the capacity, which is 0 greater than 2 on the first call.
Hint 4/4
Step 3 is wrong, and the comparison should be self.get_count() < self.__capacity.
Show solution
Test the condition on the first call rather than reading it as English.
Evaluate the guard on the first call
$$\texttt{self.get\_count()}\ \to\ 0$$
The list is empty, so the count is zero.
$$0 > 2\ \text{is False}$$
So the body does not run and the list stays empty.
Show that it can never become true
$$\text{the count only rises inside the guarded line}$$
So a guard that is False at zero is False for ever.
Independent check: offer only one book to the corrected version and it reports 1, and offer four and it still reports 2.
3§08.4 — a shared counter, an override, and a sort
A print queue. The class keeps a running total of all the pages it has ever taken, a colour job costs more a page, and the queue is sorted before it is printed.
classJob(object):
"""One print job. The class counts the pages it has taken in total."""
__total_pages = 0def__init__(self, owner, pages):
"""Assumes owner is a str and pages an int > 0."""self.__owner = owner
self.__pages = pages
Job.__total_pages = Job.__total_pages + pages
defget_owner(self):
"""Returns the name of the owner."""returnself.__owner
defget_pages(self):
"""Returns the pages in this job."""returnself.__pages
defget_total(self):
"""Returns the pages the class has taken in altogether."""return Job.__total_pages
defcost(self):
"""Returns the price of this job at 0.4 a page."""returnself.__pages * 0.4def__lt__(self, other):
"""Assumes other is a Job. The smaller job comes first."""returnself.__pages < other.__pages
def__repr__(self):
"""Returns the owner and the cost on one line."""returnself.__owner + ' ' + format(self.cost(), '.2f')
classColourJob(Job):
"""A print job in colour, which costs more a page."""def__init__(self, owner, pages):
"""Assumes owner is a str and pages an int > 0."""super().__init__(owner, pages)
defcost(self):
"""Returns the price of this job at 1.5 a page."""returnself.get_pages() * 1.5
queue = [Job('Elif', 12), ColourJob('Bora', 4), Job('Deniz', 7)]
queue.sort()
print(queue)
print(queue[0].get_total(), queue[2].get_total())
print(isinstance(queue[0], Job), isinstance(queue[2], ColourJob))
Find(a) Write the three lines this prints.
Given
The jobs are Elif with 12 pages, Bora with 4 pages in colour, and Deniz with 7 pages.
Job.cost is 0.4 a page and ColourJob.cost is 1.5 a page.
IPython console
In [1]: %run untitled0.py
The first line is the trap: the list is in order, and the numbers printed on it are not, because the order was decided by pages and the display shows cost.
Hint 1/4
Three separate things are being asked here: the order of the list, a shared number, and two questions about classes.
Hint 2/4
A sort uses __lt__, which here compares pages, while the printed form shows the cost, so the printed numbers need not be in order.
Hint 3/4
Here the pages are 12, 4 and 7, the shared total is raised by every construction including the colour one, and the costs are 4 times 1.5, 7 times 0.4 and 12 times 0.4.
Hint 4/4
The order is Bora, Deniz, Elif, the shared total is 23 from both objects asked, and the last line reports True and then False.
Show solution
Sort by the field the comparison method names, then compute the display for each item in that order.
Order by pages
$$4 < 7 < 12$$
The comparison method reads the pages, so the order is Bora, Deniz, Elif, whatever the costs are.
Work out each cost from the class the object belongs to
$$\texttt{Bora}:\ 4 \times 1.5 = 6.00$$
A ColourJob, so the override runs, and the printed form it uses is the parent's, which calls self.cost().
Independent check on the total: the three page counts are the only numbers that were ever added to it, and 12 plus 4 plus 7 is 23.
D · interleaved 3 questions
1§08.1 — a list handed to an object and changed by it
A report is built from a list of marks and then asked to drop the lowest one. The list that was handed in is printed afterwards.
classReport(object):
"""A report that keeps the list of marks it was handed."""def__init__(self, title, marks):
"""Assumes title is a str and marks a list of ints."""self.__title = title
self.__marks = marks
defget_marks(self):
"""Returns the marks of this report."""returnself.__marks
defdrop_lowest(self):
"""Removes the lowest mark and returns nothing."""self.__marks.remove(min(self.__marks))
lab_marks = [70, 45, 90]
sheet = Report('Lab total', lab_marks)
sheet.drop_lowest()
print(sheet.get_marks())
print(lab_marks)
print(lab_marks is sheet.get_marks())
Find(a) Write the three lines this prints.
Given
lab_marks is [70, 45, 90] before the report is built.
__init__ stores the list it was given.
IPython console
In [1]: %run untitled0.py
One list, two names for it, one of which is inside an object. The removal is visible outside, which is almost never what a class wants: a report that quietly edits the caller's data is the object version of the backup that was not a backup.
Hint 1/4
Count how many lists exist after the report has been built. That number settles all three printed lines at once.
Hint 2/4
An assignment stores a reference, so handing a list to a function or to an __init__ does not copy it.
Hint 3/4
Here __init__ runs self.__marks = marks, with no slice, and then drop_lowest calls remove on it.
Hint 4/4
Both printed lists are the same two marks, and is reports True.
Show solution
Ask the is question first even though it is printed last.
Count the lists
$$\texttt{self.\_\_marks = marks}$$
No slice and no list call, so nothing was built.
Apply the removal once
$$\texttt{min([70, 45, 90])} = 45$$
The smallest mark, which is what remove is then asked for.
$$\texttt{remove(45)}\ \to\ \texttt{[70, 90]}$$
In place, so both names now see two marks.
Answer $$\boxed{\texttt{[70, 90]}\ \text{twice, then True}}$$
Check
Independent check: len(lab_marks) is 2 at the end, and a class that had copied the list would have left it at 3.
2§08.1 — a default value that is a list
Two baskets, each built with one argument, and each given one item. The second parameter of __init__ has a default.
classBasket(object):
"""A shopping basket that starts empty unless a list is handed to it."""def__init__(self, owner, items=[]):
"""Assumes owner is a str and items a list of str."""self.__owner = owner
self.__items = items
defadd(self, item):
"""Assumes item is a str. Puts it in this basket."""self.__items.append(item)
defget_items(self):
"""Returns the items in this basket."""returnself.__items
first = Basket('Elif')
second = Basket('Bora')
first.add('bread')
second.add('milk')
print(first.get_items())
print(second.get_items())
print(first.get_items() is second.get_items())
Find(a) Write the three lines this prints.
Given
__init__ is declared as def __init__(self, owner, items=[]):.
Both baskets are built with one argument.
IPython console
In [1]: %run untitled0.py
One list, built once when the class definition was read, shared by every basket created without a second argument. The bread and the milk are both in it, and the two baskets are not separate at all.
Hint 1/4
Ask when the default value is built, and how many times. The answer to the second question is the answer to the whole problem.
Hint 2/4
A default value is worked out once, when the definition is read, and the same value is used by every call that leaves the argument out.
Hint 3/4
Here the default is an empty list built once, and both baskets store a reference to that one list, then append to it.
Hint 4/4
Both baskets report the same two items, and is reports True.
Show solution
Answer when rather than what. The value is obvious and the timing is the question, which is why reading the two add calls first leads nowhere.
When the default is built
$$\texttt{items=[]}\ \text{in the definition line}$$
Worked out once, as the definition is read, and kept for the lifetime of the program.
What each call stores
$$\texttt{self.\_\_items = items}$$
Both calls left the argument out, so both received the same list and both stored a reference to it.
Two appends to one list, so both baskets report two items.
Answer $$\boxed{\texttt{['bread', 'milk']}\ \text{twice, then True}}$$
Check
Independent check: build a third basket and print it before adding anything.
3§08.5 — a printed form built with format and whole division
Two training runs, each with a distance and a time. The printed form shows a pace in minutes and seconds, and the pace itself is printed afterwards as a plain number.
classRun(object):
"""One training run: the distance in metres and the seconds taken."""def__init__(self, metres, seconds):
"""Assumes metres and seconds are ints > 0."""self.__metres = metres
self.__seconds = seconds
defpace(self):
"""Returns the whole seconds needed for one kilometre."""returnself.__seconds * 1000 // self.__metres
def__repr__(self):
"""Returns the distance and the pace as minutes and seconds."""return'{0:d} m at {1:02d}:{2:02d} per km'.format(
self.__metres, self.pace() // 60, self.pace() % 60)
morning = Run(5000, 1580)
evening = Run(3000, 810)
print(morning)
print(evening)
print(morning.pace(), evening.pace())
Find(a) Write the three lines this prints.
Given
The morning run is 5000 metres in 1580 seconds.
The evening run is 3000 metres in 810 seconds.
IPython console
In [1]: %run untitled0.py
The multiplication comes before the division on purpose. Written as seconds // metres * 1000, the whole division would happen first and give 0 for both runs, because 1580 divided by 5000 is 0 with the remainder thrown away.
Hint 1/4
Work out the pace in seconds for each run before touching the printed form.
Hint 2/4
Whole division throws the remainder away, and the two pieces of a time come from dividing by 60 and taking the remainder of the same division.
Hint 3/4
Here the paces are 1580 times 1000 divided by 5000, and 810 times 1000 divided by 3000, both with whole division.
Hint 4/4
The morning pace is 316 seconds, the evening one is 270, and the two blocks read 05:16 and 04:30.
Show solution
Scale up before dividing, so that the whole division happens on a number big enough to survive it.
The multiplication first, so nothing is lost. A kilometre is a fifth of five kilometres, and a fifth of 1580 is 316.
$$810 \times 1000 = 810000,\ \ \div 3000 = 270$$
Same shape. A kilometre is a third of three, and a third of 810 is 270.
Minutes and seconds from one number
$$316 // 60 = 5,\ \ 316 \% 60 = 16$$
The same division read two ways, which is why the two lines belong together.
$$270 // 60 = 4,\ \ 270 \% 60 = 30$$
And the second run, where the 4 needs padding to two digits.
Answer $$\boxed{\texttt{5000 m at 05:16 per km};\ \texttt{3000 m at 04:30 per km};\ \texttt{316 270}}$$
Check
Independent check by size: the morning run averages a little over five minutes a kilometre and the evening one a little under four and a half, and 1580 seconds is about 26 minutes for five kilometres while 810 is about 13 and a half for three.
Mistake ledger (26 entries)
⚠ Leaving out self on the left of an assignment in __init__
The parameter is already called title, so the line title = title looks like it says the right thing and it raises nothing.
$$\boxed{\texttt{class Book(object)}\ \text{is the type};\ \ \texttt{Book('Ada', 312, 180.0)}\ \text{is one object}}$$
The class holds the design and no values. Each call of the class name builds a separate object, and self.x = v inside a method creates a slot in that object.
Both must return a string. print and str ask for __str__ and fall back to __repr__; a container asks each item for __repr__ and never falls back to __str__.
One private instance variable per data member, a class variable for anything shared, the condition inside the set method, and the printed form copied from the sample run.
Reaching the parent version of an overridden method
Both lines inside __init__, the first writing the class slot and the second copying it into the object.
Check yourself
Close the page and write, from memory: the class line, the init line and one get method for a class with two data members, without looking at any of the examples. Then the two spellings of the same private attribute, the short one and the stored one, and which places each of them works in. Then where a shared value is written and what the two sides of an assignment to it have to say. Then which method print asks for first and which one a list asks for.
Write a class with an init method taking three values, build two objects from it, and say why a change to one of them leaves the other alone?
c-class-and-object
Say what b.m(4) is short for, why the definition has one more name in its brackets than the call does, and what a method with no return statement is worth at the call?
c-methods
Write a set method that refuses a negative value, say what b.__price does from outside the class, and explain why the init method has to give the slot a value before calling the set method?
c-hiding
Say what self.__fee = Book.__fee + 1 leaves behind, in both the object and the class, and where an empty list belongs if each object needs its own?
c-class-variable
Predict what print(b) and print([b]) show for a class with only __str__, and write a printed form that matches a three line sample run including its blank line?
c-repr
Write a comparison method that orders by a count with a name as tie breaker, say which of the four comparison signs then work, and name the three list operations that start working once __eq__ exists?
c-operators
Trace which version of a method four calls reach in a tree of four classes, and say why a subclass method cannot write self.__pages for a slot the parent created?
c-inherit
Glossary (24 terms)
classsınıf
A type you define yourself, which says what data every object of that type holds and what methods it answers to.
objectnesne
The thing a program manipulates. Every object has a type, and from this section on that type may be a class you wrote, so the object carries its own values in its own slots.
örnek
Another word for an object, used when the point is which class it belongs to: an object of class Book is an instance of Book.
örneklemek
To build an object of a class by calling the class name with the values its init method expects.
attributeöznitelik
Anything reached from an object with a dot, which covers both the data it holds and the methods it answers to.
veri özniteliği
A piece of data belonging to an object or to a class, as against a method.
instance variableörnek değişkeni
A slot created by an assignment to self inside a method, so there is one of them per object.
class variablesınıf değişkeni
A slot written directly under the class line, so there is one of it for the whole class however many objects exist.
methodmetot
A function defined inside a class whose first parameter is the object it was called on.
self
The name conventionally given to the first parameter of a method, which Python fills with the object written before the dot in the call.
constructorkurucu
The init method, which runs once while a new object is being built and puts the starting values into its slots.
soyutlama
Hiding how a job is done behind a name, so that the rest of the program depends on the promise rather than on the steps.
encapsulationsarmalama
Keeping the data and the methods that work on it together in one class rather than in separate places.
information hidingbilgi gizleme
Offering a public set of methods while the data behind them is private, so that the inside of the class can change without the code that uses it changing.
private attributegizli öznitelik
An attribute written with two leading underscores, which Python stores under a longer name so that only code inside the class can use the short one.
The renaming Python applies to a double underscore name inside a class body, putting an underscore and the class name in front of it.
get method
A method whose only job is to return the value of one private data member.
set method
A method that writes one private data member, and the place where any condition the specification states belongs.
special methodözel metot
A method with a fixed name and two underscores on each side, which Python calls for you when an operator or a built in function is applied to your object.
operatör aşırı yükleme
Giving an operator a meaning for your own class by writing the special method that stands behind it.
inheritancekalıtım
Defining a class as an extension of another, so that it has every method of that class and may add to or replace them.
üst sınıf
The class another class extends, named in the brackets after the new class name.
subclassalt sınıf
A class that extends another, inheriting its methods and adding or replacing some of them.
overridegeçersiz kılma
To write a method in a subclass with the same name as one in the parent, so that a call on an object of the subclass reaches the new one.
What comes next
§09 · A simplistic Introduction to Algorithmic Complexity (Chapter 9)
This page gave a class one comparison method and then handed the sorting to the built in sort. The next sections take that away and ask how much work sorting is, and then how to write the sorting and the searching yourself. Nothing new is needed from a class for that: the algorithms call the same __lt__, and the lab that asks for a bubble sort over a list of objects is the class on this page with an algorithm written round it.
Sources
kitapJohn Guttag, Introduction to Computation and Programming Using Python, with Application to Understanding Data, second edition, chapter 8 The syllabus names this chapter for the week. The chapter also covers abstract data types as a design idea at greater length than the lecture does, and the lecture for this week spends its time on the parts a lab answer needs.
ders malzemesiThe course's own lecture slides for this week Used for the boundary of what counts as covered: abstraction and the advantages of the approach, objects and types, the class keyword, the init method and the self reference, instance variables, methods, the two string forms, encapsulation and information hiding, private attributes with get and set methods, the table of special methods behind the operators, and then inheritance with method overriding, multiple levels, the reserved word pass, isinstance, and the awkwardness of hidden attributes in a subclass.
ders malzemesiThe lab sheets for the class and the inheritance labs of this week and the next Used for the shape of an exercise, which is a list of data members, a list of methods with what each returns, a note that all data members are private, and a sample run given character for character.
ders malzemesiOne past final paper for this course, with its solutions Used for the weight and the shape of two question types: a whole question asking for a class with private data, a comparison method and a printed form together with the script that reads a file and uses it, and one part of a tracing question asking which inherited method four calls reach in a tree of four classes.
ders malzemesiOne past midterm paper for this course, with its solutions Used for the format of a tracing question, which was worth 30 of the 100 marks and asked for the exact output of four short programs, and for the list of functions and methods printed on the cover of the paper, which is why this page teaches what each operation returns rather than its name.
ders malzemesiThe course information page for one autumn term Used for the assessment weights, which are labs 20 per cent, midterm 40 and final 40, and for the recorded facts that the lowest of ten lab marks was dropped and that there was no makeup lab.
sabitThe Python 3 language reference and its library documentation Used to check the exact wording of the error messages shown on this page, the renaming rule for names with two leading underscores, the rule that a default parameter value is worked out once when the definition is read, the fallback from `__str__` to `__repr__` and the absence of the reverse fallback, the way a reflected comparison answers a greater than with a less than, and the rounding that `format` applies to an exact half.