Python Arena
Step 4 of 4

Reading and saving the data

A reading log that forgets every book when the program ends isn't much use. The books should be saved in a file, and read back the next time the program starts. Reading and writing the file is a job of its own, so it gets a class of its own: the third part of the application, next to the logic and the user interface.

Each line of the file holds one book: the title, the number of pages in the book, and then the pages of each reading session, all separated by | characters:

Dune|412|50|38
Emma|474

The title and the length are always there, but the number of sessions varies. Emma hasn't been opened yet, so its line has none.

Taking the rest of a list

When a line is split at the | characters, the first two parts are the title and the length, and whatever comes after them is the sessions, however many there are. A starred assignment takes a list apart in just this way: each variable without a star gets one item, and the one with a star gets a list of all the items that are left over.

In the Python interpreter
>>> title, length, *sessions = "Dune|412|50|38".split("|")
>>> title
'Dune'
>>> length
'412'
>>> sessions
['50', '38']
>>> title, length, *sessions = "Emma|474".split("|")
>>> sessions
[]
>>> *rest, last = [7, 8, 9]
>>> rest
[7, 8]
>>> last
9

The parts of a split line are strings, so the numbers still need int. When nothing is left over, the starred variable gets an empty list. And the star doesn't have to be on the last variable, as the last example shows: whichever variable has it gets what the others leave.

A class for the file

BookFile gets the file's name in its constructor. load reads the file and returns a list of Book objects, and save writes a list of books back, one line for each. The first time the program runs there's no file yet, so load catches the FileNotFoundError and returns an empty list: an empty log is the right start. Before the class goes into the app, it's tried out on its own:

The file books.txt
Dune|412|50|38
Emma|474
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 BookFile:
    def __init__(self, filename):
        self.__filename = filename

    def load(self):
        books = []
        try:
            with open(self.__filename) as file:
                for line in file:
                    title, length, *sessions = line.strip().split("|")
                    book = Book(title, int(length))
                    for pages in sessions:
                        book.read(int(pages))
                    books.append(book)
        except FileNotFoundError:
            pass
        return books

    def save(self, books):
        with open(self.__filename, "w") as file:
            for book in books:
                line = f"{book.title()}|{book.length()}"
                for pages in book.sessions():
                    line += f"|{pages}"
                file.write(line + "\n")


book_file = BookFile("books.txt")
books = book_file.load()
for book in books:
    print(book)
books[1].read(25)
book_file.save(books)
print(BookFile("nothing-here.txt").load())

with open("books.txt") as file:
    for line in file:
        print(line.strip())
Output
Dune: 88 of 412 pages read
Emma: 0 of 474 pages read
[]
Dune|412|50|38
Emma|474|25

load turns each line back into a Book: it creates the book with its title and length, and then lets it record each session. save does the opposite. It builds each line from the book's own methods, the title and the length first and then a | and the pages for each session.

Into the application

The app reads the file once, when it starts. From then on, the books live in its objects, and every command works with them, not with the file. When the user quits, save writes all the books back, and the new contents replace the old. ReadingLog gets two small methods for this: add(book) puts a ready-made Book in the log, and books() returns a list of all of them.

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

def books(self):
    return list(self.__books.values())

In ReadingApp, the constructor loads the books and the command quit saves them:

class ReadingApp:
    def __init__(self, storage):
        self.__storage = storage
        self.__log = ReadingLog()
        for book in self.__storage.load():
            self.__log.add(book)

    def save(self):
        self.__storage.save(self.__log.books())

    # show_commands, add_book, read and left as before. In run,
    # the command quit calls self.save() before the break.


if __name__ == "__main__":
    app = ReadingApp(BookFile("books.txt"))
    app.run()

Giving the app its storage

Notice that ReadingApp doesn't create its BookFile: it gets one as the parameter storage. The app only ever calls storage.load() and storage.save(...), so any object with those two methods will do. That makes it easy, for example, to try out the app without touching the real file at all:

class TestStorage:
    def load(self):
        return [Book("Test book", 100)]

    def save(self, books):
        print("would save", len(books), "books")


app = ReadingApp(TestStorage())

Giving an object the things it depends on from outside, instead of letting it create them itself, is called dependency injection. Which file the books are kept in, or whether there's a file at all, is decided where the app is created, and ReadingApp stays the same.

Your task

Challenge 1 of 3

Each line of the file clubs.txt holds a school club: its name, its leader and then its members, separated by commas. Fill in the blank so that the name and the leader each get one part of the line and members gets a list of the rest, and the program prints:

Chess (Ms. Park), members: 2
Choir (Mr. Diaz), members: 1
Drama (Ms. Lund), members: 0

The file clubs.txt holds:

Chess,Ms. Park,Ana,Ben
Choir,Mr. Diaz,Cy
Drama,Ms. Lund

Challenge 2

ClubFile can load the clubs from a file. Add the method save(clubs), which writes a list of Club objects to the file, replacing what was there, in the format load reads: one line for each club, with the name, the leader and then each member, separated by commas. A club with no members gets a line with just its name and leader. For example, after

club_file = ClubFile("clubs.txt")
club_file.save([Club("Chess", "Ms. Park", ["Ana", "Ben"]), Club("Drama", "Ms. Lund", [])])

the file clubs.txt should hold:

Chess,Ms. Park,Ana,Ben
Drama,Ms. Lund

Challenge 3

Put the clubs application together. Club, the storage class ClubFile and the logic class Clubs are ready in the starter code. Write the user interface, ClubApp:

  • The constructor gets a ClubFile as its parameter storage, and adds every club that storage.load() gives to the Clubs object, with its method add.
  • run first prints commands: join, show, quit, and then reads commands with the prompt > until the user types quit.
  • join asks for a club and then for a name, and adds the name to the club's members. For a club that doesn't exist it prints no such club, and asks nothing more.
  • show asks for a club and prints its members, one on each line, or no members yet when it has none. For a club that doesn't exist it prints no such club.
  • quit saves all the clubs with storage.save(...) and ends the program. Any other command prints the commands again.

Give each command a method of its own. For example, when clubs.txt holds

Chess,Ms. Park,Ana,Ben
Drama,Ms. Lund

a run could go like this:

commands: join, show, quit
> show
club: Chess
Ana
Ben
> join
club: Drama
name: Eli
> join
club: Golf
no such club
> show
club: Drama
Eli
> quit

and after it, clubs.txt holds:

Chess,Ms. Park,Ana,Ben
Drama,Ms. Lund,Eli