Python Arena
Step 3 of 4

A list of objects as an attribute

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:

  1. __init__ creates an empty list: self.songs = [].
  2. A method adds an object to the list.
  3. Other methods go through the list to work out something about all the objects.
class Song:
    def __init__(self, title, seconds):
        self.title = title
        self.seconds = seconds

    def __str__(self):
        return f"{self.title} ({self.seconds} s)"


class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []

    def add(self, song):
        self.songs.append(song)

    def total_seconds(self):
        total = 0
        for song in self.songs:
            total += song.seconds
        return total

    def print_songs(self):
        print(f"{self.name}: {len(self.songs)} songs, {self.total_seconds()} s")
        for song in self.songs:
            print(song)


road_trip = Playlist("Road trip")
road_trip.add(Song("Morning Light", 215))
road_trip.add(Song("Neon Rain", 187))
road_trip.print_songs()
Output
Road trip: 2 songs, 402 s
Morning Light (215 s)
Neon Rain (187 s)

Each 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.

A 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.

Forgetting self.

Inside a method, the list is self.songs, never plain songs. Without self., Python looks for a local variable called songs, which doesn't exist:

class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []

    def add(self, song):
        songs.append(song)


mix = Playlist("Mix")
mix.add("Neon Rain")
Python stops with
Traceback (most recent call last):
  File "main.py", line 11, in <module>
    mix.add("Neon Rain")
    ~~~~~~~^^^^^^^^^^^^^
  File "main.py", line 7, in add
    songs.append(song)
    ^^^^^
NameError: name 'songs' is not defined. Did you mean: 'self.songs'?

The 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.

Your task

Challenge 1 of 3

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:

3 books, 1086 pages

Challenge 2

A fish has a name and a length in centimeters. Write the class Aquarium, which holds fish:

  • The constructor takes no arguments (other than self); a new aquarium has no fish.
  • add(self, fish) adds a Fish object to the aquarium.
  • total_length(self) returns the combined length of all the fish.
  • __str__(self) returns a string such as fish: 2, total length: 17 cm.

For example, this code:

tank = Aquarium()
print(tank)
tank.add(Fish("Bubbles", 5))
tank.add(Fish("Nemo", 12))
print(tank.total_length())
print(tank)

prints:

fish: 0, total length: 0 cm
17
fish: 2, total length: 17 cm

Challenge 3

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.

For example, this code:

tank = Aquarium()
tank.add(Fish("Bubbles", 5))
tank.add(Fish("Nemo", 12))
tank.add(Fish("Spike", 21))
print(tank.names_longer_than(10))
print(tank.names_longer_than(30))

prints:

['Nemo', 'Spike']
[]