{"id":"larger-application-objects","slug":"objects-in-dictionary","title":"Objects in a dictionary","text":"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:\n\n```python\nclass Progress:\n    def __init__(self):\n        self.pages = 0\n\n    def read(self, pages):\n        self.pages += pages\n\n\nprogress = {\"Dune\": Progress(), \"Emma\": Progress()}\nprogress[\"Dune\"].read(50)\n\ncurrent = progress[\"Dune\"]\ncurrent.read(30)\nprint(progress[\"Dune\"].pages)\nprint(progress[\"Emma\"].pages)\nprint(current is progress[\"Dune\"])\n```\n\n```output\n80\n0\nTrue\n```\n\n`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.\n\n### A class for the data of one book\n\nIn 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.\n\nThe 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:\n\n```python\nclass Book:\n    def __init__(self, title, length):\n        self.__title = title\n        self.__length = length\n        self.__sessions = []\n\n    def title(self):\n        return self.__title\n\n    def length(self):\n        return self.__length\n\n    def sessions(self):\n        return self.__sessions\n\n    def read(self, pages):\n        self.__sessions.append(pages)\n\n    def pages_read(self):\n        return sum(self.__sessions)\n\n    def pages_left(self):\n        return self.__length - self.pages_read()\n\n    def __str__(self):\n        return f\"{self.__title}: {self.pages_read()} of {self.__length} pages read\"\n\n\nclass ReadingLog:\n    def __init__(self):\n        self.__books = {}\n\n    def add_book(self, title, length):\n        self.__books[title] = Book(title, length)\n\n    def has_book(self, title):\n        return title in self.__books\n\n    def read(self, title, pages):\n        self.__books[title].read(pages)\n\n    def pages_left(self, title):\n        return self.__books[title].pages_left()\n\n    def book(self, title):\n        return self.__books[title]\n\n\nlog = ReadingLog()\nlog.add_book(\"Dune\", 412)\nlog.read(\"Dune\", 50)\nlog.read(\"Dune\", 38)\nlog.add_book(\"Emma\", 474)\nprint(log.pages_left(\"Dune\"))\ndune = log.book(\"Dune\")\nprint(dune)\nprint(dune.sessions())\nprint(log.book(\"Emma\"))\n```\n\n```output\n324\nDune: 88 of 412 pages read\n[50, 38]\nEmma: 0 of 474 pages read\n```\n\n`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.\n\nLook 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.\n\nA 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.\n","task":"What does this program print? Type each line it prints.\n","kind":"predict","seed":"class Plant:\n    def __init__(self, height):\n        self.height = height\n\n    def grow(self, cm):\n        self.height += cm\n\n\ngarden = {\"fern\": Plant(10), \"ivy\": Plant(20)}\ngarden[\"fern\"].grow(5)\ntall = garden[\"ivy\"]\ntall.grow(3)\ngarden[\"moss\"] = tall\ngarden[\"moss\"].grow(2)\nfor name in garden:\n    print(name, garden[name].height)\n","solution":"class Plant:\n    def __init__(self, height):\n        self.height = height\n\n    def grow(self, cm):\n        self.height += cm\n\n\ngarden = {\"fern\": Plant(10), \"ivy\": Plant(20)}\ngarden[\"fern\"].grow(5)\ntall = garden[\"ivy\"]\ntall.grow(3)\ngarden[\"moss\"] = tall\ngarden[\"moss\"].grow(2)\nfor name in garden:\n    print(name, garden[name].height)\n","hints":["`tall` refers to the ivy object in the dictionary, not to a copy of it, so `tall.grow(3)` makes the ivy grow.","`garden[\"moss\"] = tall` doesn't create a new plant. The keys `ivy` and `moss` now both refer to the same object, so growing it through `moss` changes the ivy too.","The dictionary keeps the order in which the keys were added: fern, ivy, moss."],"tests":[{"name":"Output","stdin":"","expect":"fern 15\nivy 25\nmoss 25","hidden":false}],"require":[],"forbid":[],"lines":["fern 15","ivy 25","moss 25"],"more":[{"task":"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:\n\n- `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.\n- `points()` returns the team's points: 3 for each win and 1 for each draw.\n- `__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`.\n\nFor example,\n\n```python\nowls = Team(\"Owls\")\nprint(owls)\nowls.record(2, 1)\nowls.record(0, 0)\nowls.record(3, 0)\nowls.record(1, 4)\nprint(owls.points())\nprint(owls)\n```\n\nshould print:\n\n```\nOwls 0-0-0, points: 0\n7\nOwls 2-1-1, points: 7\n```\n","kind":"code","seed":"# Write the class Team here.\n\n\nif __name__ == \"__main__\":\n    owls = Team(\"Owls\")\n    owls.record(2, 1)\n    print(owls.points())\n    print(owls)\n","solution":"class Team:\n    def __init__(self, name):\n        self.__name = name\n        self.__wins = 0\n        self.__draws = 0\n        self.__losses = 0\n\n    def record(self, scored, conceded):\n        if scored > conceded:\n            self.__wins += 1\n        elif scored == conceded:\n            self.__draws += 1\n        else:\n            self.__losses += 1\n\n    def points(self):\n        return 3 * self.__wins + self.__draws\n\n    def __str__(self):\n        return f\"{self.__name} {self.__wins}-{self.__draws}-{self.__losses}, points: {self.points()}\"\n\n\nif __name__ == \"__main__\":\n    owls = Team(\"Owls\")\n    owls.record(2, 1)\n    print(owls.points())\n    print(owls)\n","hints":["The constructor stores the name and three counters that start at 0: the wins, the draws and the losses. Make them private, as in `Book`.","`record` compares the two numbers with `if`, `elif` and `else`, and adds 1 to the right counter.","`__str__` can call `self.points()` inside its f-string, so the points are counted in one place only."],"tests":[{"name":"Test 1","stdin":"","call":"owls = Team(\"Owls\")\nprint(owls)\nowls.record(2, 1)\nowls.record(0, 0)\nowls.record(3, 0)\nowls.record(1, 4)\nprint(owls.points())\nprint(owls)\n","expect":"Owls 0-0-0, points: 0\n7\nOwls 2-1-1, points: 7","hidden":false},{"name":"Test 2","stdin":"","call":"foxes = Team(\"Red Foxes\")\nfoxes.record(1, 1)\nfoxes.record(2, 2)\nprint(foxes.points())\nfoxes.record(0, 3)\nprint(foxes)\nfoxes.record(5, 4)\nprint(foxes.points())\nprint(foxes)\n","expect":"2\nRed Foxes 0-2-1, points: 2\n5\nRed Foxes 1-2-1, points: 5","hidden":true}],"require":[],"forbid":[]},{"task":"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:\n\n- `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.\n- `points(name)` returns the team's points, and 0 for a team that hasn't played yet.\n- `table()` returns a list of the teams' texts, what `str` gives for each `Team`, in the order the teams first played.\n\nFor example,\n\n```python\nleague = League()\nleague.add_match(\"Owls\", \"Foxes\", 2, 1)\nleague.add_match(\"Bears\", \"Owls\", 0, 0)\nleague.add_match(\"Foxes\", \"Bears\", 3, 1)\nprint(league.points(\"Owls\"))\nprint(league.points(\"Wolves\"))\nfor line in league.table():\n    print(line)\n```\n\nshould print:\n\n```\n4\n0\nOwls 1-1-0, points: 4\nFoxes 1-0-1, points: 3\nBears 0-1-1, points: 1\n```\n","kind":"code","seed":"class Team:\n    def __init__(self, name):\n        self.__name = name\n        self.__wins = 0\n        self.__draws = 0\n        self.__losses = 0\n\n    def record(self, scored, conceded):\n        if scored > conceded:\n            self.__wins += 1\n        elif scored == conceded:\n            self.__draws += 1\n        else:\n            self.__losses += 1\n\n    def points(self):\n        return 3 * self.__wins + self.__draws\n\n    def __str__(self):\n        return f\"{self.__name} {self.__wins}-{self.__draws}-{self.__losses}, points: {self.points()}\"\n\n\n# Write the class League here.\n\n\nif __name__ == \"__main__\":\n    league = League()\n    league.add_match(\"Owls\", \"Foxes\", 2, 1)\n    print(league.points(\"Owls\"))\n    print(league.table())\n","solution":"class Team:\n    def __init__(self, name):\n        self.__name = name\n        self.__wins = 0\n        self.__draws = 0\n        self.__losses = 0\n\n    def record(self, scored, conceded):\n        if scored > conceded:\n            self.__wins += 1\n        elif scored == conceded:\n            self.__draws += 1\n        else:\n            self.__losses += 1\n\n    def points(self):\n        return 3 * self.__wins + self.__draws\n\n    def __str__(self):\n        return f\"{self.__name} {self.__wins}-{self.__draws}-{self.__losses}, points: {self.points()}\"\n\n\nclass League:\n    def __init__(self):\n        self.__teams = {}\n\n    def __team(self, name):\n        if name not in self.__teams:\n            self.__teams[name] = Team(name)\n        return self.__teams[name]\n\n    def add_match(self, home, away, home_goals, away_goals):\n        self.__team(home).record(home_goals, away_goals)\n        self.__team(away).record(away_goals, home_goals)\n\n    def points(self, name):\n        if name not in self.__teams:\n            return 0\n        return self.__teams[name].points()\n\n    def table(self):\n        lines = []\n        for team in self.__teams.values():\n            lines.append(str(team))\n        return lines\n\n\nif __name__ == \"__main__\":\n    league = League()\n    league.add_match(\"Owls\", \"Foxes\", 2, 1)\n    print(league.points(\"Owls\"))\n    print(league.table())\n","hints":["Keep the teams in a dictionary in a private attribute. Both teams of a match may be new, so a private helper method that returns the team of a name, creating it first if it's missing, saves you from writing that `if` twice.","In `add_match`, the home team scored `home_goals` and let in `away_goals`, and for the away team it's the other way round: call `record` on each team with the numbers in its own order.","`table` goes through `self.__teams.values()` and collects `str(team)` for each team into a list. The dictionary keeps the order in which the teams were added."],"tests":[{"name":"Test 1","stdin":"","call":"league = League()\nleague.add_match(\"Owls\", \"Foxes\", 2, 1)\nleague.add_match(\"Bears\", \"Owls\", 0, 0)\nleague.add_match(\"Foxes\", \"Bears\", 3, 1)\nprint(league.points(\"Owls\"))\nprint(league.points(\"Wolves\"))\nfor line in league.table():\n    print(line)\n","expect":"4\n0\nOwls 1-1-0, points: 4\nFoxes 1-0-1, points: 3\nBears 0-1-1, points: 1","hidden":false},{"name":"Test 2","stdin":"","call":"league = League()\nprint(league.table())\nprint(league.points(\"Owls\"))\nleague.add_match(\"Lynx\", \"Hares\", 0, 2)\nleague.add_match(\"Hares\", \"Lynx\", 1, 1)\nleague.add_match(\"Hares\", \"Moles\", 4, 0)\nleague.add_match(\"Moles\", \"Lynx\", 2, 2)\nprint(league.points(\"Hares\"))\nprint(league.points(\"Lynx\"))\nfor line in league.table():\n    print(line)\n","expect":"[]\n0\n7\n2\nLynx 0-2-1, points: 2\nHares 2-1-0, points: 7\nMoles 0-1-1, points: 1","hidden":true}],"require":[{"pattern":"\\bTeam\\s*\\(","message":"Create a `Team` object for each team the league doesn't have yet."}],"forbid":[]}]}