{"id":"class-examples-search","slug":"searching","title":"A collection you can search","text":"A larger program often has two kinds of classes: one for a single thing, and one for a collection of those things. Here a `Hike` knows its own data and how to show itself, and a `Logbook` keeps the hikes someone plans to walk and answers questions about them:\n\n```python\nclass Hike:\n    def __init__(self, name, km):\n        self.name = name\n        self.km = km\n        self.walked = False\n\n    def __str__(self):\n        status = \"walked\" if self.walked else \"not yet\"\n        return f\"{self.name} ({self.km} km, {status})\"\n\n\nclass Logbook:\n    def __init__(self):\n        self.hikes = []\n\n    def add(self, name, km):\n        self.hikes.append(Hike(name, km))\n\n    def mark_walked(self, name):\n        for hike in self.hikes:\n            if hike.name == name:\n                hike.walked = True\n\n    def find(self, word):\n        found = []\n        for hike in self.hikes:\n            if word.lower() in hike.name.lower():\n                found.append(hike)\n        return found\n\n    def km_left(self):\n        total = 0\n        for hike in self.hikes:\n            if not hike.walked:\n                total += hike.km\n        return total\n\n\nsummer = Logbook()\nsummer.add(\"Lake Path\", 5)\nsummer.add(\"Ridge Loop\", 12)\nsummer.add(\"Old lake trail\", 9)\nsummer.mark_walked(\"Lake Path\")\n\nfor hike in summer.find(\"lake\"):\n    print(hike)\nprint(\"Km left:\", summer.km_left())\n```\n\n```output\nLake Path (5 km, walked)\nOld lake trail (9 km, not yet)\nKm left: 21\n```\n\nA few things to notice:\n\n- The client only calls `add` with a name and a length. `Logbook` makes the `Hike` objects itself and keeps them in its list.\n- `find` is a search: it goes through the list and collects the hikes that match into a new list. It compares in small letters, so `lake` also finds `Lake`.\n- `find` returns the `Hike` objects themselves, so the client can print them (with their `__str__`) or use their attributes.\n- `mark_walked` changes the attribute of a hike that is in the list. The list holds references to the hikes, so the change stays.\n\n### Searching for the best match\n\nAnother common search finds the one item that is best by some measure. Such a method goes through the list once and remembers the best item so far. If the list can be empty, it needs an answer for that too:\n\n```python\ndef longest(self):\n    if len(self.hikes) == 0:\n        return None\n    best = self.hikes[0]\n    for hike in self.hikes:\n        if hike.km > best.km:\n            best = hike\n    return best\n```\n\nThe examples of this part have grown step by step into something like real programs: classes that keep their data valid, show themselves with `__str__`, and answer questions about what they hold.\n","task":"What does this program print? Type each line it prints.\n","kind":"predict","seed":"class Shelf:\n    def __init__(self):\n        self.books = []\n\n    def add(self, title, pages):\n        self.books.append((title, pages))\n\n    def thin(self, max_pages):\n        found = []\n        for title, pages in self.books:\n            if pages <= max_pages:\n                found.append(title)\n        return found\n\n\nshelf = Shelf()\nshelf.add(\"Poems\", 80)\nshelf.add(\"Atlas\", 400)\nshelf.add(\"Recipes\", 150)\nprint(shelf.thin(150))\nprint(len(shelf.thin(50)))\n","solution":"class Shelf:\n    def __init__(self):\n        self.books = []\n\n    def add(self, title, pages):\n        self.books.append((title, pages))\n\n    def thin(self, max_pages):\n        found = []\n        for title, pages in self.books:\n            if pages <= max_pages:\n                found.append(title)\n        return found\n\n\nshelf = Shelf()\nshelf.add(\"Poems\", 80)\nshelf.add(\"Atlas\", 400)\nshelf.add(\"Recipes\", 150)\nprint(shelf.thin(150))\nprint(len(shelf.thin(50)))\n","hints":["`thin` collects the titles of the books with at most `max_pages` pages, in the order they were added.","150 pages is still at most 150, so Recipes is in the first list.","No book has 50 pages or fewer, so the second search returns an empty list."],"tests":[{"name":"Output","stdin":"","expect":"['Poems', 'Recipes']\n0","hidden":false}],"require":[],"forbid":[],"lines":["['Poems', 'Recipes']","0"],"more":[{"task":"The starter code has a class `Album` and a class `Collection` that keeps albums in a list. Write two search methods for `Collection`:\n\n- `by_artist(artist)` returns a list of the albums by that artist, in the order they were added.\n- `from_years(first, last)` returns a list of the albums released from the year `first` to the year `last`, both included.\n\nBoth return the `Album` objects themselves. This code:\n\n```python\nshelf = Collection()\nshelf.add(\"Blue\", \"Joni Mitchell\", 1971)\nshelf.add(\"Kind of Blue\", \"Miles Davis\", 1959)\nshelf.add(\"Court and Spark\", \"Joni Mitchell\", 1974)\nfor album in shelf.by_artist(\"Joni Mitchell\"):\n    print(album)\nfor album in shelf.from_years(1950, 1971):\n    print(album)\n```\n\nprints\n\n```\nBlue by Joni Mitchell (1971)\nCourt and Spark by Joni Mitchell (1974)\nBlue by Joni Mitchell (1971)\nKind of Blue by Miles Davis (1959)\n```\n","kind":"code","seed":"class Album:\n    def __init__(self, title, artist, year):\n        self.title = title\n        self.artist = artist\n        self.year = year\n\n    def __str__(self):\n        return f\"{self.title} by {self.artist} ({self.year})\"\n\n\nclass Collection:\n    def __init__(self):\n        self.albums = []\n\n    def add(self, title, artist, year):\n        self.albums.append(Album(title, artist, year))\n\n    # Write the methods by_artist and from_years here.\n\n\nif __name__ == \"__main__\":\n    shelf = Collection()\n    shelf.add(\"Blue\", \"Joni Mitchell\", 1971)\n    shelf.add(\"Kind of Blue\", \"Miles Davis\", 1959)\n    for album in shelf.by_artist(\"Joni Mitchell\"):\n        print(album)\n","solution":"class Album:\n    def __init__(self, title, artist, year):\n        self.title = title\n        self.artist = artist\n        self.year = year\n\n    def __str__(self):\n        return f\"{self.title} by {self.artist} ({self.year})\"\n\n\nclass Collection:\n    def __init__(self):\n        self.albums = []\n\n    def add(self, title, artist, year):\n        self.albums.append(Album(title, artist, year))\n\n    def by_artist(self, artist):\n        found = []\n        for album in self.albums:\n            if album.artist == artist:\n                found.append(album)\n        return found\n\n    def from_years(self, first, last):\n        found = []\n        for album in self.albums:\n            if first <= album.year <= last:\n                found.append(album)\n        return found\n\n\nif __name__ == \"__main__\":\n    shelf = Collection()\n    shelf.add(\"Blue\", \"Joni Mitchell\", 1971)\n    shelf.add(\"Kind of Blue\", \"Miles Davis\", 1959)\n    for album in shelf.by_artist(\"Joni Mitchell\"):\n        print(album)\n","hints":["Each method starts with an empty list, goes through `self.albums` and appends the albums that match.","An album matches `by_artist` when `album.artist == artist`, and `from_years` when `first <= album.year <= last`.","Return the list of `Album` objects. The test prints each one, which uses the `__str__` that `Album` already has."],"tests":[{"name":"Test 1","stdin":"","call":"shelf = Collection()\nshelf.add(\"Blue\", \"Joni Mitchell\", 1971)\nshelf.add(\"Kind of Blue\", \"Miles Davis\", 1959)\nshelf.add(\"Court and Spark\", \"Joni Mitchell\", 1974)\nfor album in shelf.by_artist(\"Joni Mitchell\"):\n    print(album)\nfor album in shelf.from_years(1950, 1971):\n    print(album)\n","expect":"Blue by Joni Mitchell (1971)\nCourt and Spark by Joni Mitchell (1974)\nBlue by Joni Mitchell (1971)\nKind of Blue by Miles Davis (1959)","hidden":false},{"name":"Test 2","stdin":"","call":"shelf = Collection()\nshelf.add(\"Thriller\", \"Michael Jackson\", 1982)\nshelf.add(\"Purple Rain\", \"Prince\", 1984)\nshelf.add(\"Bad\", \"Michael Jackson\", 1987)\nshelf.add(\"1999\", \"Prince\", 1982)\nfor album in shelf.by_artist(\"Prince\"):\n    print(album.title)\nfor album in shelf.from_years(1982, 1984):\n    print(album.title)\nprint(len(shelf.by_artist(\"Madonna\")), len(shelf.from_years(1990, 1999)))\n","expect":"Purple Rain\n1999\nThriller\nPurple Rain\n1999\n0 0","hidden":true}],"require":[],"forbid":[]},{"task":"Write a class `TodoList` that keeps tasks as strings. Give it these methods:\n\n- `add(task)` puts a task at the end of the list.\n- `done(task)` removes the task and returns `True`, or returns `False` when there is no such task.\n- `search(word)` returns a list of the tasks that have the word in them, whatever the capitals, in their order in the list.\n- `__str__` returns `no tasks` when the list is empty, and otherwise the word `Tasks`, the number of tasks in parentheses, a colon and the tasks separated by commas, as shown below.\n\nThis code:\n\n```python\ntodo = TodoList()\ntodo.add(\"Buy milk\")\ntodo.add(\"Call grandma\")\ntodo.add(\"Buy stamps\")\nprint(todo)\nprint(todo.search(\"buy\"))\nprint(todo.done(\"Buy milk\"), todo.done(\"Swim\"))\nprint(todo)\n```\n\nprints\n\n```\nTasks (3): Buy milk, Call grandma, Buy stamps\n['Buy milk', 'Buy stamps']\nTrue False\nTasks (2): Call grandma, Buy stamps\n```\n","kind":"code","seed":"class TodoList:\n    pass\n\n\nif __name__ == \"__main__\":\n    todo = TodoList()\n    todo.add(\"Buy milk\")\n    todo.add(\"Call grandma\")\n    print(todo)\n    print(todo.search(\"buy\"))\n","solution":"class TodoList:\n    def __init__(self):\n        self.tasks = []\n\n    def add(self, task):\n        self.tasks.append(task)\n\n    def done(self, task):\n        if task in self.tasks:\n            self.tasks.remove(task)\n            return True\n        return False\n\n    def search(self, word):\n        found = []\n        for task in self.tasks:\n            if word.lower() in task.lower():\n                found.append(task)\n        return found\n\n    def __str__(self):\n        if len(self.tasks) == 0:\n            return \"no tasks\"\n        return f\"Tasks ({len(self.tasks)}): \" + \", \".join(self.tasks)\n\n\nif __name__ == \"__main__\":\n    todo = TodoList()\n    todo.add(\"Buy milk\")\n    todo.add(\"Call grandma\")\n    print(todo)\n    print(todo.search(\"buy\"))\n","hints":["The constructor makes an empty list. `done` checks `if task in self.tasks:` before it removes anything.","In `search`, compare both in small letters: `word.lower() in task.lower()`, and collect the matches in a new list.","`__str__` returns `f\"Tasks ({len(self.tasks)}): \" + \", \".join(self.tasks)`, or `\"no tasks\"` for an empty list."],"tests":[{"name":"Test 1","stdin":"","call":"todo = TodoList()\ntodo.add(\"Buy milk\")\ntodo.add(\"Call grandma\")\ntodo.add(\"Buy stamps\")\nprint(todo)\nprint(todo.search(\"buy\"))\nprint(todo.done(\"Buy milk\"), todo.done(\"Swim\"))\nprint(todo)\n","expect":"Tasks (3): Buy milk, Call grandma, Buy stamps\n['Buy milk', 'Buy stamps']\nTrue False\nTasks (2): Call grandma, Buy stamps","hidden":false},{"name":"Test 2","stdin":"","call":"todo = TodoList()\nprint(todo)\nprint(todo.search(\"x\"), todo.done(\"x\"))\ntodo.add(\"Read PEP 8\")\ntodo.add(\"Write a class\")\ntodo.add(\"Read the class docs\")\nprint(todo.search(\"READ\"))\nprint(todo.search(\"class\"))\ntodo.done(\"Read PEP 8\")\ntodo.done(\"Write a class\")\nprint(todo)\n","expect":"no tasks\n[] False\n['Read PEP 8', 'Read the class docs']\n['Write a class', 'Read the class docs']\nTasks (1): Read the class docs","hidden":true}],"require":[],"forbid":[]}]}