{"id":"objects-as-attributes-list","slug":"list-of-objects","title":"A list of objects as an attribute","text":"You have already written classes that keep a list. Often that list holds objects of another class of yours: a playlist has songs, a team has players, a shopping cart has products. The usual pattern has three parts:\n\n1. `__init__` creates an empty list: `self.songs = []`.\n2. A method adds an object to the list.\n3. Other methods go through the list to work out something about all the objects.\n\n```python\nclass Song:\n    def __init__(self, title, seconds):\n        self.title = title\n        self.seconds = seconds\n\n    def __str__(self):\n        return f\"{self.title} ({self.seconds} s)\"\n\n\nclass Playlist:\n    def __init__(self, name):\n        self.name = name\n        self.songs = []\n\n    def add(self, song):\n        self.songs.append(song)\n\n    def total_seconds(self):\n        total = 0\n        for song in self.songs:\n            total += song.seconds\n        return total\n\n    def print_songs(self):\n        print(f\"{self.name}: {len(self.songs)} songs, {self.total_seconds()} s\")\n        for song in self.songs:\n            print(song)\n\n\nroad_trip = Playlist(\"Road trip\")\nroad_trip.add(Song(\"Morning Light\", 215))\nroad_trip.add(Song(\"Neon Rain\", 187))\nroad_trip.print_songs()\n```\n\n```output\nRoad trip: 2 songs, 402 s\nMorning Light (215 s)\nNeon Rain (187 s)\n```\n\nEach playlist gets its own empty list when it's created, because `__init__` runs once for every new object. Every method reaches the list as `self.songs`.\n\nA method can call another method of the same object, with `self.` in front: `print_songs` uses `self.total_seconds()` instead of adding up the seconds a second time. Writing each calculation once, in its own method, keeps the class short and easy to change.\n\n### Forgetting `self.`\n\nInside a method, the list is `self.songs`, never plain `songs`. Without `self.`, Python looks for a local variable called `songs`, which doesn't exist:\n\n```python\nclass Playlist:\n    def __init__(self, name):\n        self.name = name\n        self.songs = []\n\n    def add(self, song):\n        songs.append(song)\n\n\nmix = Playlist(\"Mix\")\nmix.add(\"Neon Rain\")\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 11, in <module>\n    mix.add(\"Neon Rain\")\n    ~~~~~~~^^^^^^^^^^^^^\n  File \"main.py\", line 7, in add\n    songs.append(song)\n    ^^^^^\nNameError: name 'songs' is not defined. Did you mean: 'self.songs'?\n```\n\nThe traceback shows two places: line 11, the call, and line 7 inside `add`, where the error really happened. The last place listed is usually the one to fix, and here Python even suggests the fix.\n","task":"A shelf holds books. Fill in the blanks so that `add` puts the book on the shelf and `total_pages` adds up the pages of all its books. The program should print:\n\n```\n3 books, 1086 pages\n```\n","kind":"fill","seed":"class Book:\n    def __init__(self, title, pages):\n        self.title = title\n        self.pages = pages\n\n\nclass Shelf:\n    def __init__(self):\n        self.books = []\n\n    def add(self, book):\n        self.books.[[append]](book)\n\n    def total_pages(self):\n        total = 0\n        for book in [[self.books]]:\n            total += book.pages\n        return total\n\n\nshelf = Shelf()\nshelf.add(Book(\"Small Gods\", 290))\nshelf.add(Book(\"The Quiet Sea\", 318))\nshelf.add(Book(\"Field Notes\", 478))\nprint(len(shelf.books), \"books,\", shelf.total_pages(), \"pages\")\n","solution":"class Book:\n    def __init__(self, title, pages):\n        self.title = title\n        self.pages = pages\n\n\nclass Shelf:\n    def __init__(self):\n        self.books = []\n\n    def add(self, book):\n        self.books.append(book)\n\n    def total_pages(self):\n        total = 0\n        for book in self.books:\n            total += book.pages\n        return total\n\n\nshelf = Shelf()\nshelf.add(Book(\"Small Gods\", 290))\nshelf.add(Book(\"The Quiet Sea\", 318))\nshelf.add(Book(\"Field Notes\", 478))\nprint(len(shelf.books), \"books,\", shelf.total_pages(), \"pages\")\n","hints":["`add` should put the book at the end of the list, with the list method you use for that.","The loop in `total_pages` goes through the shelf's list. Inside a method, the list is `self.books`."],"tests":[{"name":"Output","stdin":"","expect":"3 books, 1086 pages","hidden":false}],"require":[],"forbid":[],"more":[{"task":"A fish has a name and a length in centimeters. Write the class `Aquarium`, which holds fish:\n\n- The constructor takes no arguments (other than `self`); a new aquarium has no fish.\n- `add(self, fish)` adds a `Fish` object to the aquarium.\n- `total_length(self)` returns the combined length of all the fish.\n- `__str__(self)` returns a string such as `fish: 2, total length: 17 cm`.\n\nFor example, this code:\n\n```python\ntank = Aquarium()\nprint(tank)\ntank.add(Fish(\"Bubbles\", 5))\ntank.add(Fish(\"Nemo\", 12))\nprint(tank.total_length())\nprint(tank)\n```\n\nprints:\n\n```\nfish: 0, total length: 0 cm\n17\nfish: 2, total length: 17 cm\n```\n","kind":"code","seed":"class Fish:\n    def __init__(self, name, length):\n        self.name = name\n        self.length = length\n\n\nclass Aquarium:\n    pass\n\n\nif __name__ == \"__main__\":\n    tank = Aquarium()\n    print(tank)\n    tank.add(Fish(\"Bubbles\", 5))\n    tank.add(Fish(\"Nemo\", 12))\n    print(tank.total_length())\n    print(tank)\n","solution":"class Fish:\n    def __init__(self, name, length):\n        self.name = name\n        self.length = length\n\n\nclass Aquarium:\n    def __init__(self):\n        self.fish = []\n\n    def add(self, fish):\n        self.fish.append(fish)\n\n    def total_length(self):\n        total = 0\n        for fish in self.fish:\n            total += fish.length\n        return total\n\n    def __str__(self):\n        return f\"fish: {len(self.fish)}, total length: {self.total_length()} cm\"\n\n\nif __name__ == \"__main__\":\n    tank = Aquarium()\n    print(tank)\n    tank.add(Fish(\"Bubbles\", 5))\n    tank.add(Fish(\"Nemo\", 12))\n    print(tank.total_length())\n    print(tank)\n","hints":["In `__init__`, create an empty list for the fish: `self.fish = []`.","`total_length` goes through `self.fish` and adds up each fish's `length`.","`__str__` can call the other method: `f\"fish: {len(self.fish)}, total length: {self.total_length()} cm\"`."],"tests":[{"name":"Test 1","stdin":"","call":"tank = Aquarium()\nprint(tank)\ntank.add(Fish(\"Bubbles\", 5))\ntank.add(Fish(\"Nemo\", 12))\nprint(tank.total_length())\nprint(tank)\n","expect":"fish: 0, total length: 0 cm\n17\nfish: 2, total length: 17 cm","hidden":false},{"name":"Test 2","stdin":"","call":"small = Aquarium()\nbig = Aquarium()\nbig.add(Fish(\"Goldie\", 8))\nbig.add(Fish(\"Spike\", 21))\nbig.add(Fish(\"Dot\", 3))\nprint(big)\nprint(small)\n","expect":"fish: 3, total length: 32 cm\nfish: 0, total length: 0 cm","hidden":true}],"require":[],"forbid":[]},{"task":"Add the method `names_longer_than(self, limit)` to the class `Aquarium`. It returns a list of the names of the fish that are longer than `limit` centimeters, in the order they were added. If there are none, it returns an empty list.\n\nFor example, this code:\n\n```python\ntank = Aquarium()\ntank.add(Fish(\"Bubbles\", 5))\ntank.add(Fish(\"Nemo\", 12))\ntank.add(Fish(\"Spike\", 21))\nprint(tank.names_longer_than(10))\nprint(tank.names_longer_than(30))\n```\n\nprints:\n\n```\n['Nemo', 'Spike']\n[]\n```\n","kind":"code","seed":"class Fish:\n    def __init__(self, name, length):\n        self.name = name\n        self.length = length\n\n\nclass Aquarium:\n    def __init__(self):\n        self.fish = []\n\n    def add(self, fish):\n        self.fish.append(fish)\n\n\nif __name__ == \"__main__\":\n    tank = Aquarium()\n    tank.add(Fish(\"Bubbles\", 5))\n    tank.add(Fish(\"Nemo\", 12))\n    tank.add(Fish(\"Spike\", 21))\n    print(tank.names_longer_than(10))\n    print(tank.names_longer_than(30))\n","solution":"class Fish:\n    def __init__(self, name, length):\n        self.name = name\n        self.length = length\n\n\nclass Aquarium:\n    def __init__(self):\n        self.fish = []\n\n    def add(self, fish):\n        self.fish.append(fish)\n\n    def names_longer_than(self, limit):\n        names = []\n        for fish in self.fish:\n            if fish.length > limit:\n                names.append(fish.name)\n        return names\n\n\nif __name__ == \"__main__\":\n    tank = Aquarium()\n    tank.add(Fish(\"Bubbles\", 5))\n    tank.add(Fish(\"Nemo\", 12))\n    tank.add(Fish(\"Spike\", 21))\n    print(tank.names_longer_than(10))\n    print(tank.names_longer_than(30))\n","hints":["Start with an empty list of names in a local variable, then go through `self.fish`.","For each fish longer than `limit`, append its name (`fish.name`), not the fish itself.","Return the list of names after the loop."],"tests":[{"name":"Test 1","stdin":"","call":"tank = Aquarium()\ntank.add(Fish(\"Bubbles\", 5))\ntank.add(Fish(\"Nemo\", 12))\ntank.add(Fish(\"Spike\", 21))\nprint(tank.names_longer_than(10))\nprint(tank.names_longer_than(30))\n","expect":"['Nemo', 'Spike']\n[]","hidden":false},{"name":"Test 2","stdin":"","call":"tank = Aquarium()\nprint(tank.names_longer_than(0))\ntank.add(Fish(\"Ray\", 40))\ntank.add(Fish(\"Tiny\", 2))\ntank.add(Fish(\"Mid\", 15))\ntank.add(Fish(\"Edge\", 14))\nprint(tank.names_longer_than(14))\n","expect":"[]\n['Ray', 'Mid']","hidden":true}],"require":[],"forbid":[]}]}