Python Arena
Step 3 of 4

Objects in a dictionary

The values in a dictionary can be objects of your own classes too. Here each book has a Progress object that counts the pages read, and the dictionary finds it by the title:

class Progress:
    def __init__(self):
        self.pages = 0

    def read(self, pages):
        self.pages += pages


progress = {"Dune": Progress(), "Emma": Progress()}
progress["Dune"].read(50)

current = progress["Dune"]
current.read(30)
print(progress["Dune"].pages)
print(progress["Emma"].pages)
print(current is progress["Dune"])
Output
80
0
True

progress["Dune"].read(50) calls read on the object that the dictionary has for "Dune". Then the program puts that object in the variable current, and reads 30 more pages through the variable. The dictionary sees those pages too: as is shows, current and progress["Dune"] are two names for one object. Taking an object out of a dictionary never makes a copy of it.

A class for the data of one book

In the reading log, the data of one book is spread over two dictionaries: its length is in one, and the pages read in the other. add_book has to remember both, and every new piece of data would need one more dictionary. Say the reader now wants to keep each reading session, the pages read each time they sat down with the book: that would be a third dictionary, of lists, to keep in step with the other two.

The data of one book belongs together, so it gets a class of its own, Book. ReadingLog keeps a single dictionary, with the title as the key and a Book object as the value:

class Book:
    def __init__(self, title, length):
        self.__title = title
        self.__length = length
        self.__sessions = []

    def title(self):
        return self.__title

    def length(self):
        return self.__length

    def sessions(self):
        return self.__sessions

    def read(self, pages):
        self.__sessions.append(pages)

    def pages_read(self):
        return sum(self.__sessions)

    def pages_left(self):
        return self.__length - self.pages_read()

    def __str__(self):
        return f"{self.__title}: {self.pages_read()} of {self.__length} pages read"


class ReadingLog:
    def __init__(self):
        self.__books = {}

    def add_book(self, title, length):
        self.__books[title] = Book(title, length)

    def has_book(self, title):
        return title in self.__books

    def read(self, title, pages):
        self.__books[title].read(pages)

    def pages_left(self, title):
        return self.__books[title].pages_left()

    def book(self, title):
        return self.__books[title]


log = ReadingLog()
log.add_book("Dune", 412)
log.read("Dune", 50)
log.read("Dune", 38)
log.add_book("Emma", 474)
print(log.pages_left("Dune"))
dune = log.book("Dune")
print(dune)
print(dune.sessions())
print(log.book("Emma"))
Output
324
Dune: 88 of 412 pages read
[50, 38]
Emma: 0 of 474 pages read

ReadingLog now passes each job about one book on to that book's object: its read finds the book in the dictionary and lets the book record the session. Everything a book knows about itself, from its length to its sessions, is in one place.

Look at the methods add_book, has_book, read and pages_left: they take the same parameters as before and give the same answers. So ReadingApp from the previous step works with the new log without a single change. The inside of the logic changed completely, and the user interface never noticed, because it only ever used the logic's methods.

A change like this is best made in small steps that you try out one by one: first Book on its own, then ReadingLog with the test lines of the first step, which should still print what they printed before, and only then the whole app.

Your task

Challenge 1 of 3

What does this program print? Type each line it prints.

Challenge 2

A football league app needs a class for the data of one team. Write the class Team. Its constructor takes the team's name, and a new team has no wins, draws or losses yet. It has these methods:

  • record(scored, conceded) records one match: the goals the team scored and the goals it let in. Scoring more is a win, the same number a draw, and fewer a loss.
  • points() returns the team's points: 3 for each win and 1 for each draw.
  • __str__ returns the name, the numbers of wins, draws and losses with dashes between them, and the points, as in Owls 2-1-1, points: 7.

For example,

owls = Team("Owls")
print(owls)
owls.record(2, 1)
owls.record(0, 0)
owls.record(3, 0)
owls.record(1, 4)
print(owls.points())
print(owls)

should print:

Owls 0-0-0, points: 0
7
Owls 2-1-1, points: 7

Challenge 3

Now write the logic class League, which keeps Team objects in a dictionary, with the team's name as the key. Team is ready in the starter code. A League has these methods:

  • add_match(home, away, home_goals, away_goals) records a match for both teams: home scored home_goals and away scored away_goals. A team gets its Team object the first time it plays.
  • points(name) returns the team's points, and 0 for a team that hasn't played yet.
  • table() returns a list of the teams' texts, what str gives for each Team, in the order the teams first played.

For example,

league = League()
league.add_match("Owls", "Foxes", 2, 1)
league.add_match("Bears", "Owls", 0, 0)
league.add_match("Foxes", "Bears", 3, 1)
print(league.points("Owls"))
print(league.points("Wolves"))
for line in league.table():
    print(line)

should print:

4
0
Owls 1-1-0, points: 4
Foxes 1-0-1, points: 3
Bears 0-1-1, points: 1