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())
3
10 min 44 s
River Bendduration 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:
>>> 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.