{"id":"larger-application-logic","slug":"logic","title":"Start with the application logic","text":"Up to now, your programs have been a class or two, or a handful of functions. Real applications grow to hundreds or thousands of lines, and nobody can work on that much code at once. A big program stays manageable when you can work on one part of it and trust the rest to do its own job.\n\nThink of a restaurant. The cook doesn't take orders and the waiter doesn't cook, and when the soup is too salty, everyone knows whose job it is to fix it. A program can be organized the same way. Dividing it into parts that each look after one area of the work is called **separation of concerns**. Functions are one tool for it and classes are another. For classes, the idea has a name of its own, the **single responsibility principle**: give each class one kind of work, so that there's only one kind of reason to ever change it.\n\nMost applications have at least three kinds of work to divide:\n\n- the **application logic**: the data the program is about, and the rules for handling it\n- the **user interface**: everything the user sees and types\n- **storage**: keeping the data in a file between runs of the program\n\nEach of them gets a class of its own. This module builds one application this way, a step at a time: a reading log that keeps track of the books you're reading and how far you've got in each one.\n\n### Start with the logic\n\nYou can start anywhere, but the logic is a good first part. It's what the whole program is for, and you can try it out before there's any user interface at all. The reading log needs to know each book's length and how many pages of it have been read. Two dictionaries, both with the title as the key, can hold that:\n\n```python\nclass ReadingLog:\n    def __init__(self):\n        self.__lengths = {}\n        self.__pages_read = {}\n\n    def add_book(self, title, length):\n        self.__lengths[title] = length\n        self.__pages_read[title] = 0\n\n    def read(self, title, pages):\n        self.__pages_read[title] += pages\n\n    def pages_left(self, title):\n        return self.__lengths[title] - self.__pages_read[title]\n\n\nlog = ReadingLog()\nlog.add_book(\"Dune\", 412)\nlog.add_book(\"Emma\", 474)\nlog.read(\"Dune\", 50)\nlog.read(\"Dune\", 38)\nprint(log.pages_left(\"Dune\"))\nprint(log.pages_left(\"Emma\"))\n```\n\n```output\n324\n474\n```\n\nThe lines after the class are there only to try it out.\n\n### Try each method right away\n\nRun a few lines like these as soon as you've written a method. It takes a minute, and it often shows you a case you didn't think of while planning. Here, trying `read` with a book that was never added shows that the log has no answer for it:\n\n```python\nclass ReadingLog:\n    def __init__(self):\n        self.__lengths = {}\n        self.__pages_read = {}\n\n    def add_book(self, title, length):\n        self.__lengths[title] = length\n        self.__pages_read[title] = 0\n\n    def read(self, title, pages):\n        self.__pages_read[title] += pages\n\n\nlog = ReadingLog()\nlog.add_book(\"Dune\", 412)\nlog.read(\"Ulysses\", 20)\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 16, in <module>\n    log.read(\"Ulysses\", 20)\n    ~~~~~~~~^^^^^^^^^^^^^^^\n  File \"main.py\", line 11, in read\n    self.__pages_read[title] += pages\n    ~~~~~~~~~~~~~~~~~^^^^^^^\nKeyError: 'Ulysses'\n```\n\n`self.__pages_read[title]` looks for a key that isn't there. Reading a book that isn't in the log is a mistake, but sooner or later a user will make it, so the code that calls `read` needs a way to check first. That's one more small method for `ReadingLog`:\n\n```python\ndef has_book(self, title):\n    return title in self.__lengths\n```\n\nWhen the class works, delete the test lines, or move them under `if __name__ == \"__main__\":`.\n\n### The logic returns its answers\n\n`ReadingLog` never prints and never calls `input`. It gets what it needs as parameters and returns its answers, and how they're shown is up to the code that called it. That's why the same class could serve a text interface today and a web page or a test program later, without a change.\n","task":"Add the method `percent(title)` to `ReadingLog`. It returns how much of the book has been read, in percent, as a whole number rounded down. For example,\n\n```python\nlog = ReadingLog()\nlog.add_book(\"Dune\", 412)\nlog.add_book(\"Emma\", 474)\nlog.read(\"Dune\", 103)\nprint(log.percent(\"Dune\"))\nprint(log.percent(\"Emma\"))\nlog.read(\"Emma\", 474)\nprint(log.percent(\"Emma\"))\n```\n\nshould print:\n\n```\n25\n0\n100\n```\n","kind":"code","seed":"class ReadingLog:\n    def __init__(self):\n        self.__lengths = {}\n        self.__pages_read = {}\n\n    def add_book(self, title, length):\n        self.__lengths[title] = length\n        self.__pages_read[title] = 0\n\n    def has_book(self, title):\n        return title in self.__lengths\n\n    def read(self, title, pages):\n        self.__pages_read[title] += pages\n\n    def pages_left(self, title):\n        return self.__lengths[title] - self.__pages_read[title]\n\n\nif __name__ == \"__main__\":\n    log = ReadingLog()\n    log.add_book(\"Dune\", 412)\n    log.read(\"Dune\", 103)\n    print(log.percent(\"Dune\"))\n","solution":"class ReadingLog:\n    def __init__(self):\n        self.__lengths = {}\n        self.__pages_read = {}\n\n    def add_book(self, title, length):\n        self.__lengths[title] = length\n        self.__pages_read[title] = 0\n\n    def has_book(self, title):\n        return title in self.__lengths\n\n    def read(self, title, pages):\n        self.__pages_read[title] += pages\n\n    def pages_left(self, title):\n        return self.__lengths[title] - self.__pages_read[title]\n\n    def percent(self, title):\n        return self.__pages_read[title] * 100 // self.__lengths[title]\n\n\nif __name__ == \"__main__\":\n    log = ReadingLog()\n    log.add_book(\"Dune\", 412)\n    log.read(\"Dune\", 103)\n    print(log.percent(\"Dune\"))\n","hints":["The pages read are in `self.__pages_read[title]` and the book's length in `self.__lengths[title]`.","Multiply the pages read by 100 first, and then divide by the length with `//`, which rounds down. Dividing first would round down to 0 too early."],"tests":[{"name":"Test 1","stdin":"","call":"log = ReadingLog()\nlog.add_book(\"Dune\", 412)\nlog.add_book(\"Emma\", 474)\nlog.read(\"Dune\", 103)\nprint(log.percent(\"Dune\"))\nprint(log.percent(\"Emma\"))\nlog.read(\"Emma\", 474)\nprint(log.percent(\"Emma\"))\n","expect":"25\n0\n100","hidden":false},{"name":"Test 2","stdin":"","call":"log = ReadingLog()\nlog.add_book(\"Coraline\", 162)\nlog.add_book(\"Ulysses\", 730)\nlog.read(\"Coraline\", 50)\nprint(log.percent(\"Coraline\"))\nlog.read(\"Coraline\", 100)\nprint(log.percent(\"Coraline\"))\nlog.read(\"Ulysses\", 1)\nprint(log.percent(\"Ulysses\"))\nprint(log.pages_left(\"Ulysses\"))\n","expect":"30\n92\n0\n729","hidden":true}],"require":[],"forbid":[],"more":[{"task":"Write the logic class of a scoreboard for a quiz night. A `Scoreboard` keeps each team's total points, and has these methods:\n\n- `add_points(team, points)` adds points to the team's total. A team that isn't on the board yet starts from 0.\n- `points(team)` returns the team's total, and 0 for a team that isn't on the board.\n- `leader()` returns the name of the team with the most points. If two teams have the most, it's the one that came on the board first. When the board is empty, it returns `None`.\n\nFor example,\n\n```python\nboard = Scoreboard()\nprint(board.leader())\nboard.add_points(\"Owls\", 5)\nboard.add_points(\"Foxes\", 7)\nboard.add_points(\"Owls\", 4)\nprint(board.points(\"Owls\"))\nprint(board.points(\"Bears\"))\nprint(board.leader())\n```\n\nshould print:\n\n```\nNone\n9\n0\nOwls\n```\n","kind":"code","seed":"# Write the class Scoreboard here.\n\n\nif __name__ == \"__main__\":\n    board = Scoreboard()\n    board.add_points(\"Owls\", 5)\n    print(board.points(\"Owls\"))\n    print(board.leader())\n","solution":"class Scoreboard:\n    def __init__(self):\n        self.__points = {}\n\n    def add_points(self, team, points):\n        if team not in self.__points:\n            self.__points[team] = 0\n        self.__points[team] += points\n\n    def points(self, team):\n        if team not in self.__points:\n            return 0\n        return self.__points[team]\n\n    def leader(self):\n        best = None\n        for team in self.__points:\n            if best is None or self.__points[team] > self.__points[best]:\n                best = team\n        return best\n\n\nif __name__ == \"__main__\":\n    board = Scoreboard()\n    board.add_points(\"Owls\", 5)\n    print(board.points(\"Owls\"))\n    print(board.leader())\n","hints":["Keep the points in a dictionary in a private attribute: the team is the key, and its total the value.","`add_points` works like `add_sighting`: create the key with 0 if it's missing, then add with `+=`.","In `leader`, keep the best team so far in a variable that starts as `None`. Go through the teams, and replace the best one when it's still `None` or the team has more points. With `>` (not `>=`), a tie keeps the team that came first."],"tests":[{"name":"Test 1","stdin":"","call":"board = Scoreboard()\nprint(board.leader())\nboard.add_points(\"Owls\", 5)\nboard.add_points(\"Foxes\", 7)\nboard.add_points(\"Owls\", 4)\nprint(board.points(\"Owls\"))\nprint(board.points(\"Bears\"))\nprint(board.leader())\n","expect":"None\n9\n0\nOwls","hidden":false},{"name":"Test 2","stdin":"","call":"board = Scoreboard()\nboard.add_points(\"Ravens\", 3)\nboard.add_points(\"Wolves\", 8)\nboard.add_points(\"Ravens\", 5)\nprint(board.leader())\nboard.add_points(\"Hares\", 12)\nprint(board.leader())\nprint(board.points(\"Ravens\"))\nprint(board.points(\"Wolves\"))\n","expect":"Ravens\nHares\n8\n8","hidden":true}],"require":[],"forbid":[]},{"task":"The method `undone` of `TodoList` prints the tasks that aren't done yet. But showing things to the user is the user interface's job, and a logic class that prints can't be used anywhere else. Change `undone` so that it **returns** the tasks that aren't done, as a list in the order they were added, and prints nothing. For example,\n\n```python\ntodo = TodoList()\ntodo.add(\"wash the car\")\ntodo.add(\"call grandma\")\ntodo.add(\"water the plants\")\ntodo.mark_done(\"call grandma\")\nprint(todo.undone())\n```\n\nshould print:\n\n```\n['wash the car', 'water the plants']\n```\n","kind":"code","seed":"class TodoList:\n    def __init__(self):\n        self.__tasks = {}\n\n    def add(self, task):\n        self.__tasks[task] = False\n\n    def mark_done(self, task):\n        if task in self.__tasks:\n            self.__tasks[task] = True\n\n    def undone(self):\n        for task in self.__tasks:\n            if not self.__tasks[task]:\n                print(task)\n\n\nif __name__ == \"__main__\":\n    todo = TodoList()\n    todo.add(\"wash the car\")\n    todo.add(\"call grandma\")\n    todo.mark_done(\"call grandma\")\n    print(todo.undone())\n","solution":"class TodoList:\n    def __init__(self):\n        self.__tasks = {}\n\n    def add(self, task):\n        self.__tasks[task] = False\n\n    def mark_done(self, task):\n        if task in self.__tasks:\n            self.__tasks[task] = True\n\n    def undone(self):\n        tasks = []\n        for task in self.__tasks:\n            if not self.__tasks[task]:\n                tasks.append(task)\n        return tasks\n\n\nif __name__ == \"__main__\":\n    todo = TodoList()\n    todo.add(\"wash the car\")\n    todo.add(\"call grandma\")\n    todo.mark_done(\"call grandma\")\n    print(todo.undone())\n","hints":["Instead of printing each task, collect the tasks into a new list.","Start with an empty list, `append` each task that isn't done, and `return` the list after the loop."],"tests":[{"name":"Test 1","stdin":"","call":"todo = TodoList()\ntodo.add(\"wash the car\")\ntodo.add(\"call grandma\")\ntodo.add(\"water the plants\")\ntodo.mark_done(\"call grandma\")\ntodo.undone()\n","expect":"['wash the car', 'water the plants']","hidden":false,"returns":true},{"name":"Test 2","stdin":"","call":"todo = TodoList()\ntodo.undone()\n","expect":"[]","hidden":true,"returns":true},{"name":"Test 3","stdin":"","call":"todo = TodoList()\ntodo.add(\"pay rent\")\ntodo.add(\"buy bread\")\ntodo.mark_done(\"pay rent\")\ntodo.mark_done(\"buy bread\")\nprint(todo.undone())\ntodo.add(\"fix the bike\")\ntodo.mark_done(\"walk the dog\")\ntodo.undone()\n","expect":"[]\n['fix the bike']","hidden":true,"returns":true}],"require":[],"forbid":[]}]}