Python Arena
Step 1 of 3

Private attributes and derived classes

In the module on encapsulation you made attributes private by starting their names with two underscores. A private attribute can only be used in the class's own methods. The class's clients, the code that uses its objects, can't reach it, which keeps the object's data safe from careless changes.

Inheritance raises a new question: is a derived class like the class's own methods, or like any other client? For private attributes, it's like any other client. A derived class can't use its base class's private attributes either:

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

    def add(self, title, seconds):
        self.__songs.append((title, seconds))

    def count(self):
        return len(self.__songs)


class TimedPlaylist(Playlist):
    def duration(self):
        total = 0
        for title, seconds in self.__songs:
            total += seconds
        return f"{total // 60} min {total % 60} s"


mix = TimedPlaylist("Road trip")
mix.add("Highway Song", 215)
mix.add("Slow River", 187)
print(mix.count())
print(mix.duration())
Output
2
Python stops with
Traceback (most recent call last):
  File "main.py", line 25, in <module>
    print(mix.duration())
          ~~~~~~~~~~~~^^
  File "main.py", line 16, in duration
    for title, seconds in self.__songs:
                          ^^^^^^^^^^^^
AttributeError: 'TimedPlaylist' object has no attribute '_TimedPlaylist__songs'. Did you mean: '_Playlist__songs'?

The playlist keeps each song as a pair of a title and a length in seconds. add and count work, because they are Playlist's own methods. duration, which adds up the lengths, fails.

What the error message tells you

The message shows how Python hides a private attribute: it quietly renames it, putting the class's name in front. This renaming is called name mangling. Inside the class Playlist, self.__songs really means self._Playlist__songs. Inside TimedPlaylist, the same words self.__songs mean self._TimedPlaylist__songs, and the playlist has no attribute of that name.

Python's "Did you mean" points at the real name, _Playlist__songs. Writing that would work, but don't: it goes around the lock on purpose, and the code breaks as soon as someone renames the class. The renaming has a good side, too. A derived class can have a private attribute of its own with the same name as one in its base class, and the two never get mixed up.

Go through the public methods

A derived class can use everything public in its base class, just as any client can. So if the base class offers a method or a property that gives the data, the derived class uses that:

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

    def add(self, title, seconds):
        self.__songs.append((title, seconds))

    def songs(self):
        # A copy, so the private list stays safe.
        return self.__songs[:]


class TimedPlaylist(Playlist):
    def duration(self):
        total = 0
        for title, seconds in self.songs():
            total += seconds
        return f"{total // 60} min {total % 60} s"


mix = TimedPlaylist("Road trip")
mix.add("Highway Song", 215)
mix.add("Slow River", 187)
mix.add("River Bend", 242)
print(mix.duration())
Output
10 min 44 s

This is often the best way: the base class keeps control of its data, and the derived class only asks for it. When a base class has no such method and its derived classes really need the data, the base class can share it in another way, which is the topic of the next step.

Your task

Challenge 1 of 3

The method highest of MaxThermometer stops with an AttributeError, because it uses the private attribute of its base class. Fix highest so that it gets the readings through Thermometer's public method readings() instead. Don't change Thermometer. For example,

thermometer = MaxThermometer()
thermometer.add(12)
thermometer.add(17)
thermometer.add(9)
print(thermometer.highest())
print(thermometer.readings())

should print:

17
[12, 17, 9]

Challenge 2

A kid's wallet is a wallet where at most 20 euros can be spent at a time. Write a class KidsWallet that inherits Wallet, with these methods:

  • can_buy(price) returns True if the price is at most 20 and the wallet has at least that much money, and otherwise False.
  • spend(amount) overrides Wallet's: an amount over 20 isn't spent, and it returns False. Any other amount is handled by Wallet's spend.

The money is private in Wallet, so use its property money and super(). For example,

wallet = KidsWallet(30)
print(wallet.can_buy(25))
print(wallet.spend(25))
print(wallet.spend(15))
print(wallet.can_buy(18))
print(wallet.money)

should print:

False
False
True
False
15

Challenge 3

What does this program print? Type each line it prints.