← back to CS 115
Week 8Guttag §Chapter 8255 min full read
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.

def shorten(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
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.

Answer $$\boxed{\texttt{cata},\ \texttt{None},\ \texttt{<class 'NoneType'>}}$$
Check

Independent check: put print(shorten('ab', 1)) in a program of your own.

What this looks like in Spyder
The Variable Explorer tab showing a table with Name, Type, Size and Value columns for the names the program created.

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 with 98 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.

The object before the dot becomes self
$$\texttt{b.m(x)}\quad\Longleftrightarrow\quad\texttt{Book.m(b, x)}$$

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.

Two underscores rename, they do not lock
$$\texttt{self.\_\_price}\quad\to\quad\texttt{self.\_Book\_\_price}$$

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__
$$\texttt{print(b)}\to\texttt{\_\_str\_\_}\to\texttt{\_\_repr\_\_}\quad\texttt{print([b])}\to\texttt{\_\_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
$$\texttt{AudioBook}\ \to\ \texttt{Book}\ \to\ \texttt{object}$$

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
  1. 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.

  2. 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.

  3. 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
  1. Build a class with an __init__ method and create several objects from it, each with its own values in its own slots.

  2. 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.

  3. 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.

  4. Distinguish a class variable from an and predict what two objects of the class share after a method has written to it.

  5. Give a class a printed form with __repr__ and predict what a single object and a list of objects show on the screen.

  6. Define the special methods that make the comparison operators, addition, sort, min and in work on objects of your own class.

  7. 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

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__.

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.

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.

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.

Apply both changes to that one list

$$\texttt{append(55)}\ \to\ \texttt{[70, 45, 90, 55]}$$

In place, and the call hands back None, which the program does not keep.

$$\texttt{sort()}\ \to\ \texttt{[45, 55, 70, 90]}$$

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
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.

Answer $$\boxed{\texttt{['ada', 'milo', 'zeta']},\ \texttt{None},\ \texttt{['ada', 'milo', 'zeta']}}$$
Check

Independent check: first is words would be False and first == words would be True at the end.

Notation
symbolreads asmeanswatch 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:

catalogue = [['Ada', 312, 180.0], ['Zeno', 98, 75.5]]
print(catalogue[1][0], catalogue[1][1])

Sample Run:

Zeno 98
Why it fails

The 1 means Zeno and the 0 means the title, and nothing says so. The day a field is inserted, every position after it is wrong and nothing is raised.

DefinitionDefinition 8.1: class, object, instance variable
Conditions
  • 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.

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.

class Book(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 312 180.0
Zeno 98 75.5
<class '__main__.Book'>
FindWhat each object holds, and what type the objects are.
Given
  • __init__ takes three values besides self.

  • Two objects are built, from different values.

Solution

Read the definition line as a promise about slots

$$\texttt{def \_\_init\_\_(self, title, pages, price):}$$

Four parameters, of which the first is the object being built.

$$\texttt{self.title = title}$$

Chosen over a different slot name because the caller thinks in the same words.

Read a call as build and hand back

$$\texttt{b1 = Book('Ada', 312, 180.0)}$$

Three arguments for four parameters: Python fills self in.

$$\texttt{b2 = Book('Zeno', 98, 75.5)}$$

A second call, a second object. The two share nothing, which is the whole point of the repair.

Answer $$\boxed{\texttt{Ada 312 180.0}\ /\ \texttt{Zeno 98 75.5}\ /\ \texttt{<class '\_\_main\_\_.Book'>}}$$
Check

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.

class Book(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.0
for b in catalogue:
    total = total + b.price
print('average price:', format(total / len(catalogue), '.2f'))

Sample Run:

shortest book: Zeno with 98 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.

Answer $$\boxed{\texttt{shortest book: Zeno with 98 pages}\ /\ \texttt{average price: 125.17}}$$
Check

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.

class Room(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 - 4
print(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
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.

$$\texttt{6 + 20 = 26}$$

The sum reads both objects after the change.

Answer $$\boxed{\texttt{B12 6}\ /\ \texttt{B30 20}\ /\ 26}$$
Check

Independent check by size: the two seat counts start at 6 and 24, and four seats are removed once, so the total has to be 30 minus 4.

⚠ 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.

wrong$$\texttt{def \_\_init\_\_(self, title):}\ \ \texttt{title = title}$$
right$$\texttt{def \_\_init\_\_(self, title):}\ \ \texttt{self.title = title}$$
⚠ Calling __init__ by name

It is a method with a name, so Book.__init__('Ada', 312) looks like the way to build a book.

wrong$$\texttt{b = Book.\_\_init\_\_('Ada', 312)}$$
right$$\texttt{b = Book('Ada', 312)}$$
⚠ Treating the class as an object

The class has the fields written inside it, so it looks as though the values are in there too.

wrong$$\texttt{print(Book.title)}$$
right$$\texttt{b = Book('Ada', 312)}\ \ \texttt{print(b.title)}$$

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.

$$\boxed{\texttt{b.m(x)}\ \equiv\ \texttt{Book.m(b, x)}}$$

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.

Looks like this, but is not

A method that needs nothing from the object needs no self in its parameter list.

class Book(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

    def chapters():
        """Returns the number of chapters, or tries to."""
        return 12


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.

class Book(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

    def reading_days(self, per_day):
        """Assumes per_day is an int > 0.
        Returns the whole days needed at per_day pages a day."""
        return self.pages // per_day + 1

    def discount(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.

class Book(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

    def get_title(self):
        """Returns the title."""
        return self.__title

    def get_pages(self):
        """Returns the page count."""
        return self.__pages

    def longer_than(self, other):
        """Assumes other is a Book.
        Returns True when this book has more pages than the other one."""
        return self.__pages > other.__pages

    def pages_between(self, other):
        """Assumes other is a Book.
        Returns the difference in page count, never a negative number."""
        return abs(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))

Sample Run:

True
False
214
214
FindThe four printed lines.
Given
  • Ada is 312 pages and Zeno is 98.

  • Each method is called once in each direction.

Solution

Read the two objects apart

$$\texttt{self.\_\_pages}\ \text{against}\ \texttt{other.\_\_pages}$$

Inside the class both are allowed, because the renaming of a double underscore name happens per class and both objects are of this class.

Notice which method cares about direction

$$\texttt{first.longer\_than(second)}\ \to\ \texttt{True}$$

312 is more than 98. Turning the call round turns the answer round, because the comparison is not symmetric.

$$\texttt{pages\_between}\ \to\ 214\ \text{both ways}$$

The abs makes this one symmetric on purpose, which is the kind of decision the docstring has to record.

Answer $$\boxed{\texttt{True}\ /\ \texttt{False}\ /\ 214\ /\ 214}$$
Check

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.

class Meter(object):
    """A water meter with a running reading."""

    def __init__(self, reading):
        """Assumes reading is an int >= 0."""
        self.reading = reading

    def add(self, units):
        """Assumes units is an int >= 0. Raises the reading."""
        self.reading = self.reading + units

    def double(self):
        """Returns twice the reading without changing it."""
        return self.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
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.

Answer $$\boxed{\texttt{None},\ 150,\ 300,\ 150}$$
Check

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.

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.

class Book(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

    def get_price(self):
        """Returns the price."""
        return self.__price


b = Book('Ada', 180.0)
print(b.get_price())
b._Book__price = -99.0
print(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.

class Book(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.0
        self.set_price(price)

    def get_title(self):
        """Returns the title."""
        return self.__title

    def get_pages(self):
        """Returns the page count."""
        return self.__pages

    def get_price(self):
        """Returns the price."""
        return self.__price

    def set_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.

Watch the condition decide

$$\texttt{set\_price(-5.0)}\ \to\ \text{no change}$$

The if is False, so the body does not run.

$$\texttt{set\_price(150.0)}\ \to\ 150.0$$

The condition holds and the slot is replaced.

Answer $$\boxed{180.0\ \to\ 180.0\ \to\ 150.0}$$
Check

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.

class Book(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

    def get_title(self):
        """Returns the title."""
        return self.__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.

class Card(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.0
        self.set_balance(balance)

    def get_balance(self):
        """Returns the balance."""
        return self.__balance

    def set_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
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.

wrong$$\texttt{if p > 0: b.set\_price(p)}$$
right$$\texttt{def set\_price(self, p):}\ \ \texttt{if p > 0: self.\_\_price = p}$$
⚠ Calling the set method in __init__ with no slot in place

The specification says to initialise through the set method, and a first value that fails the condition then leaves the object without the slot.

wrong$$\texttt{def \_\_init\_\_(self, p):}\ \ \texttt{self.set\_price(p)}$$
right$$\texttt{self.\_\_price = 0.0}\ \ \texttt{self.set\_price(p)}$$
⚠ 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.

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.

class Book(object):
    """A book whose method writes the shared fine through self."""

    __fine_per_day = 2.5

    def __init__(self, title):
        """Assumes title is a str."""
        self.__title = title

    def raise_fine(self, amount):
        """Assumes amount is a number. Meant to raise the shared fine."""
        self.__fine_per_day = Book.__fine_per_day + amount

    def fine(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.0 5.0
4.0 2.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.

class Book(object):
    """A book whose method writes the shared fine on the class."""

    __fine_per_day = 2.5

    def __init__(self, title):
        """Assumes title is a str."""
        self.__title = title

    def raise_fine(self, amount):
        """Assumes amount is a number. Raises the fine every book shares."""
        Book.__fine_per_day = Book.__fine_per_day + amount

    def fine(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.0 8.0
FindThe fine each of the two books reports for two days.
Given
  • The shared fine starts at 2.5.

  • raise_fine(1.5) is called on the first book only.

Solution

Write on the class, not on the object

$$\texttt{Book.\_\_fine\_per\_day = Book.\_\_fine\_per\_day + amount}$$

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.

class Shelf(object):
    """A shelf of titles. The list was created in the wrong place."""

    __titles = []

    def add(self, title):
        """Assumes title is a str. Puts it on this shelf."""
        Shelf.__titles.append(title)

    def get_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.

class Shelf(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 = []

    def add(self, title):
        """Assumes title is a str. Puts it on this shelf."""
        self.__titles.append(title)

    def get_titles(self):
        """Returns the titles on this shelf."""
        return self.__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.

$$\texttt{['Ada', 'Zeno']}\ \text{twice, and}\ \texttt{True}$$

Both appends reached the same list, and is confirms it rather than being guessed at.

Count them in the second

$$\texttt{self.\_\_titles = []}\ \text{in \_\_init\_\_}$$

Evaluated once per object, so two objects means two lists.

$$\texttt{['Ada']}\ \text{and}\ \texttt{['Zeno']},\ \text{and}\ \texttt{False}$$

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__.

class Printer(object):
    """A campus printer. The page price is the same everywhere."""

    __page_price = 0.4

    def __init__(self, place):
        """Assumes place is a str."""
        self.__place = place
        self.__pages = 0

    def print_pages(self, pages):
        """Assumes pages is an int >= 0. Adds them to this printer."""
        self.__pages = self.__pages + pages

    def owed(self):
        """Returns what this printer has taken in."""
        return self.__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
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.

Answer $$\boxed{\texttt{4.80 2.00}\ /\ \texttt{False}}$$
Check

Independent check by ratio: 12 pages against 5 pages should give amounts in the ratio 12 to 5, and 4.8 divided by 2.0 is 2.4, which is 12 over 5.

⚠ Writing a shared value through self

self is the object and the object is of the class, so the left hand side looks equivalent.

wrong$$\texttt{self.\_\_fee = Book.\_\_fee + 1}$$
right$$\texttt{Book.\_\_fee = Book.\_\_fee + 1}$$
⚠ A list as a class variable

It looks like a tidy place for a default empty list, and the first object behaves correctly.

wrong$$\texttt{class Shelf:}\ \ \texttt{\_\_titles = []}$$
right$$\texttt{def \_\_init\_\_(self):}\ \ \texttt{self.\_\_titles = []}$$
⚠ A per object value written as a class variable

Both kinds are written inside the class body, and a page count of 0 looks like a sensible default to put there.

wrong$$\texttt{class Printer:}\ \ \texttt{\_\_pages = 0}$$
right$$\texttt{def \_\_init\_\_(self):}\ \ \texttt{self.\_\_pages = 0}$$

8.5Giving the class a printed form with __repr__

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.

$$\boxed{\texttt{print(b)}\to\texttt{\_\_str\_\_}\to\texttt{\_\_repr\_\_}\to\text{address};\ \ \texttt{print([b])}\to\texttt{\_\_repr\_\_}\to\text{address}}$$

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.

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.

class Book(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.

class Book(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

    def get_title(self):
        """Returns the title."""
        return self.__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.

Solution

Build the string, converting as you go

$$\texttt{'Pages: ' + str(self.\_\_pages) + '\\n'}$$

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.

class Book(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."""
        return self.__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.

Solution

The two that ask for the friendly form

$$\texttt{print(b)}\ \text{and}\ \texttt{str(b)}$$

Both reach __str__ because it exists. If it did not, both would fall back to __repr__ and print the other line.

The two that ask for the exact form

$$\texttt{repr(b)}\ \to\ \texttt{Book('Ada', 312)}$$

The repr of the title keeps its quotation marks, which is what makes the exact form exact: it shows a string as a string.

$$\texttt{print([b, b])}$$

The list asks each item for __repr__ and ignores __str__ entirely, which is why a class with only a friendly form breaks down inside a list.

Answer $$\boxed{\texttt{Ada (312 pages)}\ \text{twice, then}\ \texttt{Book('Ada', 312)}\ \text{twice}}$$
Check

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.

class Seat(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."""
        return str(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
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.

Answer $$\boxed{\texttt{1A}\ /\ \texttt{[1A, 14F]}\ /\ \texttt{14F taken}}$$
Check

Independent check: the list line shows no quotation marks round 1A, which tells you the text came from a printed form and not from a list of strings.

⚠ A printed form that prints

The method is about showing the object, and printing is how things are shown everywhere else in the course.

wrong$$\texttt{def \_\_repr\_\_(self): print(self.\_\_t)}$$
right$$\texttt{def \_\_repr\_\_(self): return self.\_\_t}$$
⚠ Joining a number to text without converting it

The value is printed as text everywhere else, so it looks like text.

wrong$$\texttt{return 'Pages: ' + self.\_\_pages}$$
right$$\texttt{return 'Pages: ' + str(self.\_\_pages)}$$
⚠ Defining only __str__ when the script prints a list

Printing one object works perfectly, so the class looks finished until the script prints the whole catalogue.

wrong$$\texttt{def \_\_str\_\_(self):}\ \text{only}$$
right$$\texttt{def \_\_repr\_\_(self):}\ \text{, or both}$$
⚠ Leaving the newline out of a form the sample run ends with one

On the screen one block looks right either way, and the difference only shows when several objects are printed.

wrong$$\texttt{return 'Pages: ' + str(p)}$$
right$$\texttt{return 'Pages: ' + str(p) + '\\n'}$$

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.

$$\boxed{\texttt{a < b}\ \equiv\ \texttt{a.\_\_lt\_\_(b)};\ \ \texttt{L.sort()}\ \text{calls it};\ \ \texttt{e in L}\ \text{calls}\ \texttt{\_\_eq\_\_}}$$

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.

Looks like this, but is not

Once __lt__ is written the class knows how to be ordered, so all four comparison signs should work.

class Duration(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 // 60
        self.__minutes = minutes % 60

    def __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:

True
False
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.

class Book(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

    def get_title(self):
        """Returns the title."""
        return self.__title

    def get_pages(self):
        """Returns the page count."""
        return self.__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."""
        if self.__pages < other.__pages:
            return True
        if self.__pages == other.__pages and self.__title < other.__title:
            return True
        return False

    def __repr__(self):
        """Returns the title and the page count on one line."""
        return self.__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))

Sample Run:

True
[Zeno/98, Bora/204, Milo/204, Ada/312]
Zeno/98
FindThe three printed lines.
Given
  • The books are Milo 204, Ada 312, Zeno 98 and Bora 204.

  • __lt__ compares pages first and titles when the pages are equal.

Solution

Write the order as two tests, not one

$$\texttt{if self.\_\_pages < other.\_\_pages: return True}$$

The main field. Returning early keeps the second test for the case it is meant for.

$$\texttt{if ... == ... and self.\_\_title < other.\_\_title}$$

The tie breaker, and the equality in front of it is what stops it from overruling the page count.

Read the three results

$$\texttt{shelf[0] < shelf[1]}\ \to\ \texttt{True}$$

Milo at 204 against Ada at 312: the first test settles it.

$$\texttt{sort()}\ \to\ \texttt{[Zeno/98, Bora/204, Milo/204, Ada/312]}$$

Bora comes before Milo, and only the second test could have put it there.

Answer $$\boxed{\texttt{True};\ \texttt{[Zeno/98, Bora/204, Milo/204, Ada/312]};\ \texttt{Zeno/98}}$$
Check

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.

class Duration(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 // 60
        self.__minutes = minutes % 60

    def get_hours(self):
        """Returns the whole hours."""
        return self.__hours

    def get_minutes(self):
        """Returns the minutes left over."""
        return self.__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:50 02:25
04:15
True
[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.

Solution

Do the carry in one place

$$\texttt{self.\_\_hours = hours + minutes // 60}$$

Put in __init__ rather than in __add__, so that Duration(0, 135) is also correct.

$$\texttt{self.\_\_minutes = minutes \% 60}$$

The two lines are the same division read two ways, which is why they belong next to each other.

Add without changing either side

$$\texttt{return Duration(1+2, 50+25)}$$

A new object, so lab and lecture are untouched and can be printed again afterwards.

$$\texttt{Duration(3, 75)}\ \to\ \texttt{04:15}$$

The carries the 75 minutes, which is the reason the addition itself needs no carry of its own.

Answer $$\boxed{\texttt{01:50 02:25};\ \texttt{04:15};\ \texttt{True};\ \texttt{[00:45, 01:50, 03:05]}}$$
Check

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.

class Team(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."""
        if self.__points < other.__points:
            return True
        if self.__points == other.__points and self.__name < other.__name:
            return True
        return False

    def __repr__(self):
        """Returns the name and the points on one line."""
        return self.__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
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.

Answer $$\boxed{\texttt{[Zeytin(3), Ada(7), Kartal(7)]}\ /\ \texttt{True}}$$
Check

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.

wrong$$\texttt{shelf.sort()}\ \text{with no}\ \texttt{\_\_lt\_\_}$$
right$$\texttt{def \_\_lt\_\_(self, other):}\ \ \texttt{return ...}$$
⚠ A comparison written the wrong way round

Both sides are there and it compiles, so the only sign is that the sorted list comes out reversed.

wrong$$\texttt{return other.\_\_pages < self.\_\_pages}$$
right$$\texttt{return self.\_\_pages < other.\_\_pages}$$
⚠ A tie breaker with no equality in front of it

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.

$$\boxed{\text{search order}:\ \texttt{AudioBook}\ \to\ \texttt{Book}\ \to\ \texttt{object};\ \ \text{first match runs}}$$

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.

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.

class Book(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

    def get_pages(self):
        """Returns the page count."""
        return self.__pages


class AudioBook(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

    def pages_per_minute(self):
        """Returns how many pages one minute of audio covers."""
        return self.__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
    return self.__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.

class Book(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

    def get_title(self):
        """Returns the title."""
        return self.__title

    def get_pages(self):
        """Returns the page count."""
        return self.__pages

    def reading_hours(self):
        """Returns the whole hours needed at 40 pages an hour."""
        return self.__pages // 40

    def __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')


class AudioBook(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

    def get_reader(self):
        """Returns the name of the person reading it aloud."""
        return self.__reader

    def reading_hours(self):
        """Returns the whole hours of audio, replacing the page estimate."""
        return self.__minutes // 60

    def __repr__(self):
        """Returns the Book lines, then the reader."""
        return super().__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
7 3
2
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.

Solution

Let the parent set up its own data

$$\texttt{super().\_\_init\_\_(title, author, pages)}$$

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.

Override the one answer that differs

$$\texttt{AudioBook.reading\_hours}\ \to\ \texttt{205 // 60 = 3}$$

Same name as the parent's method, so the search stops here for an audio book.

$$\texttt{Book.reading\_hours(audio)}\ \to\ \texttt{98 // 40 = 2}$$

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.

class Book(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

    def get_title(self):
        """Returns the title."""
        return self.__title

    def __lt__(self, other):
        """Assumes other is a Book. Compares the titles alphabetically."""
        return self.__title < other.__title


class AudioBook(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."""
        return self.__minutes < other.__minutes


class Reserve(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:

True True
False
True True
True
False
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.

Solution

What isinstance answers

$$\texttt{isinstance(short, AudioBook)}\ \text{and}\ \texttt{isinstance(short, Book)}$$

Both True. An object of a subclass is an object of the parent class too, which is what lets one list hold all four of these.

$$\texttt{isinstance(paper, AudioBook)}\ \to\ \texttt{False}$$

The relation only runs one way: a Book is not an AudioBook.

Which comparison method each pair uses

$$\texttt{short < long\_one}\ \to\ \texttt{205 < 640}$$

Both are AudioBooks, so the subclass version runs and compares minutes.

$$\texttt{paper < short}\ \to\ \texttt{'Milo' < 'Ada'}$$

The left hand object is a Book, so the search starts in Book and its version runs, comparing titles.

Answer $$\boxed{\texttt{True True};\ \texttt{False};\ \texttt{True True};\ \texttt{True};\ \texttt{False};\ \texttt{Bora}}$$
Check

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


class Loan(object):
    """One loan, with the day it went out and the day it came back."""

    __daily_fine = 2.0

    def __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 = None

    def get_taken(self):
        """Returns the day the book went out."""
        return self.__taken

    def set_returned(self, returned):
        """Assumes returned is a date written as YYYYmmdd."""
        self.__returned = datetime.datetime.strptime(returned,
                                                     '%Y%m%d').date()

    def days_out(self):
        """Returns the days the book was out, and zero while it is still out."""
        if self.__returned is None:
            return 0
        return (self.__returned - self.__taken).days

    def fine(self):
        """Returns the fine owed for each day past the fourteenth."""
        late = self.days_out() - 14
        if late <= 0:
            return 0.0
        return 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())

Sample Run:

Title: Ada
Taken: 2024-10-11
Returned: 2024-11-02
Fine: 16.00

Title: Zeno
Taken: 2024-10-17
Returned: None
Fine: 0.00

22 0
FindThe two blocks and the two day counts.
Given
  • Ada went out on the eleventh of October and came back on the second of November; Zeno went out on the seventeenth of October and is still out.

  • The fine is 2.0 for each day past the fourteenth.

Solution

Turn text into a date, once

$$\texttt{datetime.datetime.strptime(taken, '\%Y\%m\%d').date()}$$

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.

class Fee(object):
    """The plain fee for one library loan."""

    def __init__(self, days):
        """Assumes days is an int >= 0."""
        self.__days = days

    def get_days(self):
        """Returns how many days the loan ran over."""
        return self.__days

    def amount(self):
        """Returns the fee at two lira a day."""
        return self.__days * 2

    def __repr__(self):
        """Returns the fee with the word lira after it."""
        return str(self.amount()) + ' lira'


class ReserveFee(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

    def amount(self):
        """Returns the fee at five lira an hour."""
        return self.__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
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.

Where the text comes from

$$\texttt{ReserveFee}\ \text{has no}\ \texttt{\_\_repr\_\_}$$

So the search goes up one step and Fee's version runs.

Where the number comes from

$$\texttt{self.amount()}\ \text{with self a ReserveFee}$$

The search starts at the object's own class and finds the override, so the hours version runs.

$$\texttt{4 \times 5 = 20}$$

The hours, not the days. The days are untouched and still 3.

Answer $$\boxed{\texttt{6 lira},\ \texttt{20 lira},\ 3}$$
Check

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.

wrong$$\texttt{def \_\_init\_\_(self, t, p, m):}\ \ \texttt{self.\_\_minutes = m}$$
right$$\texttt{super().\_\_init\_\_(t, p)}\ \ \texttt{self.\_\_minutes = m}$$
⚠ Reaching the parent's private attribute from the subclass

The object has the data and the subclass is of the parent's class, so the short name looks available.

wrong$$\texttt{return self.\_\_pages / self.\_\_minutes}$$
right$$\texttt{return self.get\_pages() / self.\_\_minutes}$$
⚠ Passing self to super

The older form Book.__init__(self, t, p) does take self, so the two spellings get mixed.

wrong$$\texttt{super().\_\_init\_\_(self, t, p)}$$
right$$\texttt{super().\_\_init\_\_(t, p)}$$
⚠ Copying the parent's printed form into the child

It works on the day it is written, and the child's version then stops following the parent's format.

wrong$$\texttt{return 'Title: ' + ... + 'Read by: ' + ...}$$
right$$\texttt{return super().\_\_repr\_\_() + 'Read by: ' + ...}$$
Turning a written specification into a class

Every exercise this week begins with a list of data members, a list of methods and a sample run.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  1. 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.

  2. 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__.

  3. 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.

  4. 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.

  1. 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.

  2. Run __init__ once per box, filling its slots

    Take the arguments in order, remembering that self is filled in for you.

  3. 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.

  4. 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.

class Book(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

    def get_title(self):
        """Returns the title."""
        return self.__title


class AudioBook(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

    def get_minutes(self):
        """Returns how many minutes of audio there are."""
        return self.__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.

class Book(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

    def get_title(self):
        """Returns the title."""
        return self.__title


class AudioBook(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

    def get_minutes(self):
        """Returns how many minutes of audio there are."""
        return self.__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
    return self.__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 inherited method finds nothing

$$\texttt{get\_title}\ \text{reads}\ \texttt{self.\_Book\_\_title}$$

The method is Book's, so it spells the name Book's way, and no line ever created that slot.

$$\text{AttributeError naming}\ \texttt{\_Book\_\_title}$$

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
  1. 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.

  2. 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.

  3. Write one get method per readable member, and a set method only where the specification asks, with its condition inside the set method.

  4. 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.

  5. Write the printed form last, copying the labels from the sample run and converting every number with str or format.

  6. 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.

class Locker(object):
    """A dormitory locker rented for a term."""

    __deposit = 250.0

    def __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.0
        self.set_monthly(monthly)

    def get_number(self):
        """Returns the locker number."""
        return self.__number

    def get_holder(self):
        """Returns the name of the student renting it."""
        return self.__holder

    def get_monthly(self):
        """Returns the monthly rent."""
        return self.__monthly

    def set_monthly(self, monthly):
        """Assumes monthly is a number.
        Sets the rent only when the value is positive."""
        if monthly > 0:
            self.__monthly = monthly

    def get_deposit(self):
        """Returns the deposit every locker shares."""
        return Locker.__deposit

    def term_cost(self, months):
        """Assumes months is an int >= 0.
        Returns the rent for that many months plus the deposit."""
        return self.__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.

$$\texttt{self.\_\_number},\ \texttt{self.\_\_holder},\ \texttt{self.\_\_monthly}$$

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.

Put the condition inside the set method

$$\texttt{if monthly > 0: self.\_\_monthly = monthly}$$

Inside, so that every caller is covered. The call with -10.0 later is refused by this line and by nothing else.

$$\texttt{95.5}\ \text{survives the refusal}$$

The second block still shows 95.50, which is the only visible sign that the condition ran.

Read the shared value through the class name

$$\texttt{return Locker.\_\_deposit}$$

Chosen over self.__deposit, which would also work today.

$$\texttt{120.0 \times 4 + 250.0 = 730.0}$$

The term cost of the first locker: four months of rent plus the deposit once.

$$\texttt{95.5 \times 4 + 250.0 = 632.0}$$

And the second. The deposit is added once, not once a month, because the specification says plus the deposit.

Write the printed form against the sample run

$$\texttt{'Locker: ' + str(self.\_\_number) + '\\n'}$$

The number is an int, so str is needed.

$$\texttt{format(self.\_\_monthly, '.2f')}$$

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.

class Tally(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 = 0

    def get_count(self):
        """Returns the count so far."""
        return self.__count

    def add(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."""
        return self.__label + ': ' + str(self.__count)


gate = Tally('entrance')
gate.add(4)
gate.add(3)
print(gate)
print(gate.get_count())

Sample Run:

entrance: 7
7
  1. 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.

  2. __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.

  3. 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.

  4. 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.

  5. 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.

class Ticket(object):
    """One cinema ticket. Every ticket shares the same booking fee."""

    __fee = 5.0

    def __init__(self, film, seat, price):
        """Assumes film and seat are str, price a float."""
        self.__film = film
        self.__seat = seat
        self.__price = price

    def total(self):
        """Returns the price with the booking fee added."""
        return self.__price + Ticket.__fee

    def raise_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.

  1. Put the booking fee where the specification puts it: one value for every ticket, written under the class line.

  2. Store the three fields of a ticket privately in __init__.

  3. The total is the price of this ticket plus the shared fee, read through the class name.

  4. Raising the fee adds the amount to the shared value.

  5. Cheapest first, so a ticket is less than another when its price is the lower one.

  6. 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
  1. (a) Write the class Court.

  2. (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}$$

Shared by every court, so one slot for the class.

$$\texttt{\_\_number},\ \texttt{\_\_surface},\ \texttt{\_\_hourly}\ \text{in \_\_init\_\_}$$

Each court has its own three.

Init through the set method

$$\texttt{self.\_\_hourly = 0.0}$$

A safe value before the set method runs, so a refused first rate still leaves a slot to read.

$$\texttt{self.set\_hourly(hourly)}$$

The specification asks for this, which is what makes the condition apply to the caller's value as well as to later ones.

Answer $$\boxed{120.00\ \ 170.00\ \ \texttt{True}\ \ 160.00}$$
Check

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()


class Delivery(object):
    """One parcel waiting to go out. Every parcel shares one rate."""

    __rate = 12.5

    def __init__(self, code, city, weight):
        """Assumes code and city are str, weight a float."""
        self.__code = code
        self.__city = city
        self.__weight = 0.0
        self.set_weight(weight)

    def get_code(self):
        """Returns the parcel code."""
        return self.__code

    def get_city(self):
        """Returns the destination city."""
        return self.__city

    def get_weight(self):
        """Returns the weight in kilograms."""
        return self.__weight

    def set_weight(self, weight):
        """Assumes weight is a number.
        Sets the weight only when the value is positive."""
        if weight > 0:
            self.__weight = weight

    def get_rate(self):
        """Returns the rate per kilogram that every parcel shares."""
        return Delivery.__rate

    def cost(self):
        """Returns the price of sending this parcel."""
        return self.__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."""
        if self.__weight < other.__weight:
            return True
        if self.__weight == other.__weight and self.__code < other.__code:
            return True
        return False

    def __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')


def load_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


def total_cost(parcels):
    """Assumes parcels is a list of Delivery objects.
    Returns what the whole list costs to send."""
    total = 0.0
    for 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.

$$\texttt{self.\_\_weight = 0.0}\ \text{then}\ \texttt{self.set\_weight(weight)}$$

The safe value first, so a refused weight leaves a slot to read.

Part one: the order, as two tests

$$\texttt{if self.\_\_weight < other.\_\_weight: return True}$$

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.

$$\texttt{Delivery(field[0], field[1], float(field[2]))}$$

Every field out of a file is text, so the weight needs converting here rather than inside the class, which should be able to assume a number.

Part two: the three answers the script prints

$$\texttt{van.sort()}\ \to\ \texttt{TR104, TR221, TR005, TR087}$$

The sort calls the comparison method, and the first two are the tied pair in code order.

$$\texttt{43.75 + 43.75 + 90.625 + 150.0 = 328.125}$$

Each cost is the weight times 12.5. The total is printed as 328.12, which is dealt with in the check below.

$$\texttt{van[len(van) - 1].get\_city()}\ \to\ \texttt{Izmir}$$

After a sort that puts the lightest first, the heaviest is the last item.

Answer $$\boxed{\text{TR104, TR221, TR005, TR087};\ \ 328.12;\ \ \texttt{Izmir}}$$
Check

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.

class Book(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

    def get_price(self):
        """Returns the price."""
        return self.__price


b = Book('Ada', 180.0)
print(b.get_price())
b._Book__price = -99.0
print(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.

Ask what the stored name is

$$\texttt{self.\_\_price}\ \to\ \texttt{self.\_Book\_\_price}$$

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.

class Book(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

    def get_pages(self):
        """Returns the page count."""
        return self.__pages


class AudioBook(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

    def pages_per_minute(self):
        """Returns how many pages one minute of audio covers."""
        return self.__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
    return self.__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.

Work out what the failing line asks for

$$\texttt{self.\_\_pages}\ \text{inside AudioBook}$$

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.

class Book(object):
    """A book whose method writes the shared fine through self."""

    __fine_per_day = 2.5

    def __init__(self, title):
        """Assumes title is a str."""
        self.__title = title

    def raise_fine(self, amount):
        """Assumes amount is a number. Meant to raise the shared fine."""
        self.__fine_per_day = Book.__fine_per_day + amount

    def fine(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.0 5.0
4.0 2.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.

Read the two slots

$$\texttt{b1.\_Book\_\_fine\_per\_day}\ \to\ 4.0$$

The write did happen, and it happened inside the object, so the description claiming nothing happened is out.

$$\texttt{Book.\_Book\_\_fine\_per\_day}\ \to\ 2.5$$

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.

class Lamp(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 = 5
print(desk.level)
brighter = row[1]
brighter.level = brighter.level + 2
print(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
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.

$$\texttt{brighter}\ \text{and}\ \texttt{row[1]}$$

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.

class Tag(object):
    """A luggage tag with a code on it."""

    def __init__(self, code):
        """Assumes code is a str."""
        self.__code = code

    def get_code(self):
        """Returns the code."""
        return self.__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
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.

The two that find the friendly form

$$\texttt{print(t)}\ \text{and}\ \texttt{str(t)}$$

Both ask for __str__, which exists, so both print [AB12].

The one that does not fall back

$$\texttt{repr(t)}$$

Asks for __repr__, which is missing. There is no rule sending it to __str__, so the default text is produced.

$$\texttt{repr(t)[:26]}\ \to\ \texttt{'<\_\_main\_\_.Tag object at 0x'}$$

True. Only the first 26 characters are compared because the rest of that text is an address and changes on every run.

Answer $$\boxed{\texttt{[AB12]},\ \texttt{[AB12]},\ \texttt{True}}$$
Check

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.

class Queue(object):
    """A queue ticket. The class counts how many it has handed out."""

    __issued = 0

    def __init__(self, name):
        """Assumes name is a str. Counts this ticket as it is made."""
        self.__name = name
        Queue.__issued = Queue.__issued + 1
        self.__number = Queue.__issued

    def get_number(self):
        """Returns the number printed on this ticket."""
        return self.__number

    def get_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
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.

Raise the shared count, once per ticket

$$\texttt{Queue.\_\_issued}:\ 0 \to 1 \to 2 \to 3$$

The class name is on the left, so all three raises land on the one slot.

Copy it into the ticket

$$\texttt{self.\_\_number}\ \to\ 1,\ 2,\ 3$$

Read at the moment of building, and never updated afterwards, so each ticket keeps its own.

$$\texttt{get\_issued()}\ \to\ 3\ \text{from both}$$

Reads the shared slot, which has moved on since either ticket was built.

Answer $$\boxed{\texttt{1 2 3}\ /\ \texttt{3 3}}$$
Check

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.

class Box(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."""
        return self.__litres < other.__litres

    def __repr__(self):
        """Returns the label and the volume on one line."""
        return self.__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
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.

The two list lines

$$\texttt{sorted(store)}\ \to\ \texttt{[blue:15, red:40, green:60]}$$

A new list in order of volume, decided by __lt__.

$$\texttt{store}\ \to\ \texttt{[red:40, blue:15, green:60]}$$

Unchanged, because nothing in this program called sort.

The two comparison lines

$$\texttt{min(store)}\ \to\ \texttt{blue:15}$$

Walks the list asking the less than question, so it needs no extra method.

$$\texttt{store[0] > store[1]}\ \to\ \texttt{True}$$

No __gt__ exists, so Python asks store[1] < store[0], which is 15 against 40.

Answer $$\boxed{\texttt{[blue:15, red:40, green:60]}\ /\ \texttt{[red:40, blue:15, green:60]}\ /\ \texttt{blue:15}\ /\ \texttt{True}}$$
Check

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.

class Word(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

    def get_text(self):
        """Returns the word as it was given."""
        return self.__text

    def __eq__(self, other):
        """Assumes other is a Word. Compares the two in lower case."""
        return self.__text.lower() == other.__text.lower()

    def __repr__(self):
        """Returns the word as it was given."""
        return self.__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
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.

What the comparison does

$$\texttt{self.\_\_text.lower() == other.\_\_text.lower()}$$

Both sides lowered, so the capitals in the bag and in the question are both irrelevant.

The three list operations

$$\texttt{Word('ADA') in bag}\ \to\ \texttt{True}$$

Walks the bag until the comparison succeeds, at position 0.

$$\texttt{bag.index(Word('MILO'))}\ \to\ 2$$

The same walk, reporting where rather than whether.

Answer $$\boxed{\texttt{True},\ 2,\ \texttt{True False},\ 1}$$
Check

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.

class Pen(object):
    """The base price rule for anything you write with."""

    def cost(self, n):
        """Assumes n is an int. Returns the cost of n items."""
        return n * 3


class Marker(Pen):
    """Priced exactly like a pen, so nothing is added."""
    pass


class Pencil(Pen):
    """Priced by the item with a fixed box charge on top."""

    def cost(self, n, extra=10):
        """Assumes n is an int. Returns the cost of n items plus the box."""
        return n + extra


class Crayon(Marker):
    """Priced like a marker, one lira off."""

    def cost(self, n):
        """Assumes n is an int. Returns the cost of n items, less one lira."""
        return n - 1


print(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
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.

Draw the two branches

$$\texttt{Pen}\ \to\ \texttt{Marker}\ \to\ \texttt{Crayon}$$

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.

class Shelf(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 = []

    def get_count(self):
        """Returns how many books are on the shelf."""
        return len(self.__books)

    def add(self, title):
        """Assumes title is a str.
        Adds the book only while there is still room."""
        if self.get_count() > self.__capacity:
            self.__books.append(title)

    def __repr__(self):
        """Returns the code and the count on one line."""
        return self.__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 0
0

Below is the reasoning the author wrote out. One of the five steps is wrong.

Find
  1. (a) Which step is wrong?

  2. (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.

Answer $$\boxed{\text{Step 3};\ \ \texttt{if self.get\_count() < self.\_\_capacity:}}$$
Check

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.

class Job(object):
    """One print job. The class counts the pages it has taken in total."""

    __total_pages = 0

    def __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

    def get_owner(self):
        """Returns the name of the owner."""
        return self.__owner

    def get_pages(self):
        """Returns the pages in this job."""
        return self.__pages

    def get_total(self):
        """Returns the pages the class has taken in altogether."""
        return Job.__total_pages

    def cost(self):
        """Returns the price of this job at 0.4 a page."""
        return self.__pages * 0.4

    def __lt__(self, other):
        """Assumes other is a Job. The smaller job comes first."""
        return self.__pages < other.__pages

    def __repr__(self):
        """Returns the owner and the cost on one line."""
        return self.__owner + ' ' + format(self.cost(), '.2f')


class ColourJob(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)

    def cost(self):
        """Returns the price of this job at 1.5 a page."""
        return self.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
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().

$$\texttt{Deniz}:\ 7 \times 0.4 = 2.80$$

A plain Job.

Answer $$\boxed{\texttt{[Bora 6.00, Deniz 2.80, Elif 4.80]};\ \ \texttt{23 23};\ \ \texttt{True False}}$$
Check

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.

class Report(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

    def get_marks(self):
        """Returns the marks of this report."""
        return self.__marks

    def drop_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
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.

class Basket(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

    def add(self, item):
        """Assumes item is a str. Puts it in this basket."""
        self.__items.append(item)

    def get_items(self):
        """Returns the items in this basket."""
        return self.__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
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.

$$\texttt{append('bread')}\ \text{then}\ \texttt{append('milk')}$$

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.

class Run(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

    def pace(self):
        """Returns the whole seconds needed for one kilometre."""
        return self.__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
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 two paces

$$1580 \times 1000 = 1580000,\ \ \div 5000 = 316$$

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.

wrong$$\texttt{def \_\_init\_\_(self, title):}\ \ \texttt{title = title}$$
right$$\texttt{def \_\_init\_\_(self, title):}\ \ \texttt{self.title = title}$$
⚠ Calling __init__ by name

It is a method with a name, so Book.__init__('Ada', 312) looks like the way to build a book.

wrong$$\texttt{b = Book.\_\_init\_\_('Ada', 312)}$$
right$$\texttt{b = Book('Ada', 312)}$$
⚠ Treating the class as an object

The class has the fields written inside it, so it looks as though the values are in there too.

wrong$$\texttt{print(Book.title)}$$
right$$\texttt{b = Book('Ada', 312)}\ \ \texttt{print(b.title)}$$
⚠ 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)}$$
⚠ 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.

wrong$$\texttt{if p > 0: b.set\_price(p)}$$
right$$\texttt{def set\_price(self, p):}\ \ \texttt{if p > 0: self.\_\_price = p}$$
⚠ Calling the set method in __init__ with no slot in place

The specification says to initialise through the set method, and a first value that fails the condition then leaves the object without the slot.

wrong$$\texttt{def \_\_init\_\_(self, p):}\ \ \texttt{self.set\_price(p)}$$
right$$\texttt{self.\_\_price = 0.0}\ \ \texttt{self.set\_price(p)}$$
⚠ 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}$$
⚠ Writing a shared value through self

self is the object and the object is of the class, so the left hand side looks equivalent.

wrong$$\texttt{self.\_\_fee = Book.\_\_fee + 1}$$
right$$\texttt{Book.\_\_fee = Book.\_\_fee + 1}$$
⚠ A list as a class variable

It looks like a tidy place for a default empty list, and the first object behaves correctly.

wrong$$\texttt{class Shelf:}\ \ \texttt{\_\_titles = []}$$
right$$\texttt{def \_\_init\_\_(self):}\ \ \texttt{self.\_\_titles = []}$$
⚠ A per object value written as a class variable

Both kinds are written inside the class body, and a page count of 0 looks like a sensible default to put there.

wrong$$\texttt{class Printer:}\ \ \texttt{\_\_pages = 0}$$
right$$\texttt{def \_\_init\_\_(self):}\ \ \texttt{self.\_\_pages = 0}$$
⚠ A printed form that prints

The method is about showing the object, and printing is how things are shown everywhere else in the course.

wrong$$\texttt{def \_\_repr\_\_(self): print(self.\_\_t)}$$
right$$\texttt{def \_\_repr\_\_(self): return self.\_\_t}$$
⚠ Joining a number to text without converting it

The value is printed as text everywhere else, so it looks like text.

wrong$$\texttt{return 'Pages: ' + self.\_\_pages}$$
right$$\texttt{return 'Pages: ' + str(self.\_\_pages)}$$
⚠ Defining only __str__ when the script prints a list

Printing one object works perfectly, so the class looks finished until the script prints the whole catalogue.

wrong$$\texttt{def \_\_str\_\_(self):}\ \text{only}$$
right$$\texttt{def \_\_repr\_\_(self):}\ \text{, or both}$$
⚠ Leaving the newline out of a form the sample run ends with one

On the screen one block looks right either way, and the difference only shows when several objects are printed.

wrong$$\texttt{return 'Pages: ' + str(p)}$$
right$$\texttt{return 'Pages: ' + str(p) + '\\n'}$$
⚠ 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.

wrong$$\texttt{shelf.sort()}\ \text{with no}\ \texttt{\_\_lt\_\_}$$
right$$\texttt{def \_\_lt\_\_(self, other):}\ \ \texttt{return ...}$$
⚠ A comparison written the wrong way round

Both sides are there and it compiles, so the only sign is that the sorted list comes out reversed.

wrong$$\texttt{return other.\_\_pages < self.\_\_pages}$$
right$$\texttt{return self.\_\_pages < other.\_\_pages}$$
⚠ A tie breaker with no equality in front of it

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\_\_}$$
⚠ 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.

wrong$$\texttt{def \_\_init\_\_(self, t, p, m):}\ \ \texttt{self.\_\_minutes = m}$$
right$$\texttt{super().\_\_init\_\_(t, p)}\ \ \texttt{self.\_\_minutes = m}$$
⚠ Reaching the parent's private attribute from the subclass

The object has the data and the subclass is of the parent's class, so the short name looks available.

wrong$$\texttt{return self.\_\_pages / self.\_\_minutes}$$
right$$\texttt{return self.get\_pages() / self.\_\_minutes}$$
⚠ Passing self to super

The older form Book.__init__(self, t, p) does take self, so the two spellings get mixed.

wrong$$\texttt{super().\_\_init\_\_(self, t, p)}$$
right$$\texttt{super().\_\_init\_\_(t, p)}$$
⚠ Copying the parent's printed form into the child

It works on the day it is written, and the child's version then stops following the parent's format.

wrong$$\texttt{return 'Title: ' + ... + 'Read by: ' + ...}$$
right$$\texttt{return super().\_\_repr\_\_() + 'Read by: ' + ...}$$
Formula card
A class, an object, and an instance variable
$$\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.

The method call and the self parameter
$$\boxed{\texttt{b.m(x)}\ \equiv\ \texttt{Book.m(b, x)}}$$

The definition has one parameter more than the call has arguments, and the first one is the object.

Private attributes, and get and set as the way in
$$\boxed{\texttt{self.\_\_price}\ \to\ \texttt{self.\_Book\_\_price}\ \ \text{so}\ \ \texttt{b.\_\_price}\ \text{is an AttributeError}}$$

Two leading underscores are stored with the class name in front. From outside only the methods work.

Class variable against instance variable
$$\boxed{\texttt{Book.\_\_fee = v}\ \text{changes the shared slot};\ \ \texttt{self.\_\_fee = v}\ \text{makes a new one}}$$

Written under the class line means one slot for the class. A read through self finds it; a write through self makes a separate slot in one object.

The two printed forms and which is asked for
$$\boxed{\texttt{print(b)}\to\texttt{\_\_str\_\_}\to\texttt{\_\_repr\_\_}\to\text{address};\ \ \texttt{print([b])}\to\texttt{\_\_repr\_\_}\to\text{address}}$$

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__.

The operator is the method
$$\boxed{\texttt{a < b}\ \equiv\ \texttt{a.\_\_lt\_\_(b)};\ \ \texttt{L.sort()}\ \text{calls it};\ \ \texttt{e in L}\ \text{calls}\ \texttt{\_\_eq\_\_}}$$

__lt__ powers the less than sign, the greater than sign, sort, sorted and min, and not <=.

The search order for a method
$$\boxed{\text{search order}:\ \texttt{AudioBook}\ \to\ \texttt{Book}\ \to\ \texttt{object};\ \ \text{first match runs}}$$

The search starts at the class of the object and walks up the parents; the first match runs.

The skeleton of a class from a specification
$$\texttt{class C(object)}\ \to\ \texttt{\_\_init\_\_}\ \to\ \texttt{get/set}\ \to\ \texttt{\_\_repr\_\_}\ \to\ \text{behaviour}$$

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
$$\texttt{super().m()}\ \ \text{or}\ \ \texttt{Parent.m(obj)}$$

The first form only inside the subclass, with no self in the brackets.

Turning a file of records into a list of objects
$$\texttt{field = line.strip().split(',')}\ \to\ \texttt{L.append(C(field[0], int(field[1])))}$$

Strip before split, convert every number, and open and close the file around the loop.

A two part order inside one comparison method
$$\texttt{if a < b: True}\ \ \texttt{if a == b and c < d: True}\ \ \texttt{False}$$

The second test is guarded by an equality on the first field, and self is on the left in both.

Numbering objects as they are built
$$\texttt{C.\_\_made = C.\_\_made + 1}\ \ \texttt{self.\_\_no = C.\_\_made}$$

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.

Spotted something missing or wrong? tell us · share your own notes or an old exam.

Last updated .