Start learning free

LESSON 564 OF 569 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Playlist in Python: Two Classes Working Together

Practice O(n) Read the full explanation

A music app keeps playlists: songs in the order they were added. Build it from two classes that work together. A Song holds one song's data. A Playlist keeps a list of Song objects and answers questions about them.

Implement:

  • Song(title, seconds) holds a song's title and its length in seconds;
  • Playlist() starts with no songs. Its methods:
    • add(song) adds a Song at the end of the playlist;
    • remove(title) removes the first song with that title and returns True; if no song has that title, it changes nothing and returns False;
    • total_seconds() returns the sum of the songs' lengths in seconds (0 for an empty playlist);
    • duration() returns the same total as a string "m:ss": the minutes, a colon, then the seconds with two digits. 5 seconds are "0:05", 750 seconds are "12:30", and 3600 seconds are "60:00": the minutes are never padded.

The starter code spells every name the way your language does.

The input is a list of operations, applied in order to one new playlist. The output lists what the remove, total_seconds and duration calls return, in order, with booleans printed as True and False.

 

Example 1:

Input:
    add(Song("Intro", 65))
    add(Song("Sunrise", 200))
    total_seconds()
    duration()
    remove("Intro")
    duration()
Output: 265 4:25 True 3:20
Explanation: The two songs last 65 + 200 = 265 seconds: 4 minutes (240 seconds) and 25 seconds.
After "Intro" is removed, only the 200 seconds of "Sunrise" are left: 3 minutes and 20 seconds.

Example 2:

Input:
    duration()
    remove("Rain")
    total_seconds()
Output: 0:00 False 0
Explanation: The playlist is empty: it lasts 0 seconds, and there is no "Rain" to remove.

Example 3:

Input:
    add(Song("Echo", 30))
    add(Song("Echo", 45))
    add(Song("Rain", 5))
    remove("Echo")
    total_seconds()
    remove("Echo")
    remove("Echo")
    duration()
Output: True 50 True False 0:05
Explanation: Two songs are called "Echo". The first remove takes only the first one, the 30 seconds,
so 45 + 5 = 50 seconds are left. The second remove takes the other "Echo", and the third finds none.
Only "Rain" is left: 5 seconds, written 0:05.

 

Constraints:

  • 1 <= seconds <= 3600
  • A title is one word of letters and digits. Titles are compared exactly: "Rain" and "rain" are different titles.
  • At most 200 operations.
STEP 1 OF 15
    Ctrl + Enter to check

    PYTHON · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Playlist in Python, explained

    Problem overview

    A music app keeps playlists: songs in the order they were added. The lesson builds it from two classes. A Song(title, seconds) holds one song's title and length. A Playlist() starts empty and keeps a list of Song objects. Its methods are:

    • add(song): adds a song at the end.
    • remove(title): removes the first song with that title and returns True. If no song has that title, it changes nothing and returns False.
    • total_seconds(): the sum of the songs' lengths, 0 for an empty playlist.
    • duration(): the same total as "m:ss". The seconds have two digits and the minutes are never padded: 5 seconds are "0:05", 750 seconds are "12:30" and 3600 seconds are "60:00".

    With songs of 65 and 200 seconds, the total is 265 and the duration is 4:25. After removing the 65-second song, the duration is 3:20.

    Approach: one class holds data, the other holds the list

    Song is a small class that only stores two values. Playlist owns a list of songs in self._songs. The underscore says that the list is private to the class by convention: only the playlist's own methods change it, so it cannot get out of step with what the methods report. Song's attributes stay public because Playlist reads each song's title and length.

    Two of the methods have a catch. remove must take out only the first match, so it returns as soon as it finds one. duration must turn a number of seconds into minutes and seconds, using // for the whole minutes and % for the seconds left over.

    Step by step

    1. Write Song.__init__ to store self.title and self.seconds.
    2. Start the playlist with self._songs = [] in its __init__.
    3. add: append the song to self._songs. The playlist keeps the song object itself, with its title and length together.
    4. remove: loop over the indexes with range(len(self._songs)), because removing goes by position. If self._songs[i].title == title, call self._songs.pop(i) and return True. Returning at once stops the loop, so only the first song with that title goes.
    5. After the loop, return False: no song matched and the list is unchanged.
    6. total_seconds: start total at 0, add each song.seconds and return the total.
    7. duration: call self.total_seconds() so the adding stays in one place. Then take minutes = total // 60 and seconds = total % 60.
    8. Pad the seconds. If seconds < 10, return the minutes, ":0" and the seconds, each number made a string with str. Otherwise return the minutes, ":" and the seconds.

    The solution in Python

    class Song:
        def __init__(self, title, seconds):
            self.title = title
            self.seconds = seconds
    
    class Playlist:
        def __init__(self):
            self._songs = []
    
        def add(self, song):
            self._songs.append(song)
    
        def remove(self, title):
            for i in range(len(self._songs)):
                if self._songs[i].title == title:
                    self._songs.pop(i)
                    return True
            return False
    
        def total_seconds(self):
            total = 0
            for song in self._songs:
                total += song.seconds
            return total
    
        def duration(self):
            total = self.total_seconds()
            minutes = total // 60
            seconds = total % 60
            if seconds < 10:
                return str(minutes) + ":0" + str(seconds)
            return str(minutes) + ":" + str(seconds)
    

    Using it:

    playlist = Playlist()
    playlist.add(Song("Intro", 65))
    playlist.add(Song("Sunrise", 200))
    print(playlist.total_seconds(), playlist.duration())
    print(playlist.remove("Intro"), playlist.duration())
    
    265 4:25
    True 3:20
    

    Two songs can share a title. Here remove("Echo") takes only one of them each time:

    playlist = Playlist()
    playlist.add(Song("Echo", 30))
    playlist.add(Song("Echo", 45))
    playlist.add(Song("Rain", 5))
    print(playlist.remove("Echo"), playlist.total_seconds())
    print(playlist.remove("Echo"), playlist.remove("Echo"))
    print(playlist.duration())
    
    True 50
    True False
    0:05
    

    The first call removes the 30-second song, leaving 45 + 5 = 50 seconds. The second removes the other Echo, and the third finds none, so it returns False. Only Rain is left: 5 seconds, written 0:05.

    Why duration calls total_seconds

    Both methods need the total length. If duration repeated the loop, a change to the way songs are counted would have to be made twice. Calling self.total_seconds() keeps that rule in one place, and duration is left with one job: formatting. Methods of a class can call each other through self in this way, just like they read its attributes.

    The playlist also shows why a class holds a list of other objects. Playlist never copies a song's title or seconds into its own attributes. It keeps the Song objects and asks each one when it needs the data, so a song is always one object with both values together. It also means remove can compare titles with self._songs[i].title == title and, once it finds a match, take the whole song out in one step.

    Formatting the duration

    Some totals and their durations: 5 gives 0:05, 59 gives 0:59, 60 gives 1:00, 750 gives 12:30 and 3600 gives 60:00. The padding rule only needs a check for seconds below 10, because the other values already have two digits. The minutes stay as they are, so an hour shows as 60 minutes, not 1:00:00.

    Edge cases and common mistakes

    • Empty playlist. total_seconds() is 0, duration() is "0:00" and remove("Rain") returns False.
    • Titles are case-sensitive. After the playlist above, remove("rain") returns False, because "rain" and "Rain" are different titles.
    • Forgetting to return after pop. Without the return True, the loop goes on and could remove a second song with the same title, or fail when the list is shorter than the range it started with.
    • Removing while looping over the songs. Deleting from a list you are iterating with for song in self._songs skips elements. Loop over indexes and return right after the removal, as the solution does.
    • Adding a number to a string. minutes + ":" fails with a TypeError when minutes is an integer. Use str(minutes).
    • Unpadded seconds. Without the extra 0, 5 seconds would read 0:5.
    Keep going with Python500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free