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"])
80
0
Trueprogress["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"))
324
Dune: 88 of 412 pages read
[50, 38]
Emma: 0 of 474 pages readReadingLog 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.