Python Arena
Step 2 of 3

Protected attributes

Private attributes are hidden from derived classes too, and sometimes that's more hiding than you want. A base class that is written to be extended often has data its derived classes need to work with, data that clients still shouldn't touch. Many programming languages have a level of access for this, called protected: hidden from clients, open to derived classes.

Python has no keyword for it. Instead there's a convention, an agreement among Python programmers: a name that starts with one underscore is protected. Python itself doesn't stop anyone from using it. The underscore is a sign to other programmers: this is for the class and its derived classes, keep out.

Here's the timed playlist again, this time with a protected list of songs. The derived class gets a second method too, longest, which finds the longest song:

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"

    def longest(self):
        best_title = ""
        best_seconds = 0
        for title, seconds in self._songs:
            if seconds > best_seconds:
                best_title = title
                best_seconds = seconds
        return best_title


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

duration and longest can now use self._songs directly, without asking for a copy first. Python doesn't rename a name with one underscore, so self._songs is the same attribute in both classes.

Only an agreement

Code outside the classes could use a protected attribute too, and Python would let it. A private one is another matter:

In the Python interpreter
>>> class Note:
...     def __init__(self):
...         self._draft = "buy milk"
...         self.__pin = "1234"
...
>>> note = Note()
>>> note._draft
'buy milk'
>>> note.__pin
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Note' object has no attribute '__pin'

A programmer who sees the underscore knows the attribute isn't meant for them. If they use it anyway, and the class later changes how it stores its data, their code breaks, and it's their own fault.

Three levels of access

Public, protected and private are known as access modifiers: they set who may use an attribute or a method.

Access Written as The class itself Derived classes Clients
Public self.name yes yes yes
Protected self._name yes yes no, by agreement
Private self.__name yes no no

Which one to pick? Make public what clients need to use. Make an attribute private when not even a derived class should change it directly, such as a balance that must never go below zero. Make it protected when the class is meant to be extended and the derived classes need the data to do their job. Derived classes then share the duty of keeping the data valid, so it's a good habit to make changes through the base class's methods, such as add, and use the protected attribute mostly for reading, as duration and longest do.

Your task

Challenge 1 of 3

The base class keeps its items in a protected list. Fill in the blank so that the derived class returns the items in alphabetical order, and the program prints:

['map', 'snacks', 'water']

Challenge 2

The method running_low of ShopInventory stops with an AttributeError. Change the stock in Inventory from private to protected, everywhere it's used, so that ShopInventory can use it. running_low(limit) returns the items that have fewer than limit left. For example,

shop = ShopInventory()
shop.add("apples", 12)
shop.add("pears", 3)
shop.add("plums", 5)
print(shop.running_low(6))
print(shop.count("pears"))

should print:

['pears', 'plums']
3

Challenge 3

A course keeps the names of its students in a protected list. Write a class LimitedCourse that inherits Course, for courses with room for only a limited number of students:

  • The constructor takes the course's name and limit, the most students the course can have.
  • is_full() returns True when the course has limit students.
  • enroll(student) overrides Course's: when the course is full, it enrolls nobody and returns False. Otherwise it works as Course's enroll does.

For example,

yoga = LimitedCourse("Yoga", 2)
print(yoga.enroll("Ada"))
print(yoga.enroll("Ada"))
print(yoga.enroll("Ben"))
print(yoga.is_full())
print(yoga.enroll("Cy"))
print(yoga)

should print:

True
False
True
True
False
Yoga: 2 students