Python Arena
Step 3 of 3

Protected methods

Methods have the same three levels of access as attributes. Most methods are public: they are what the class offers its clients. Classes often have helper methods too, small methods that the class's other methods call to do part of their work. In the module on encapsulation such helpers were private, with two underscores. A private method can't be called from a derived class, for the same reason that a private attribute can't be used there: Python renames it after the class it's written in. A helper that derived classes should be able to use is made protected, with one underscore.

Here a receipt keeps its prices in cents, as whole numbers, so that the sums are always exact. Its protected helper _euros turns cents into text. The derived class DiscountReceipt adds a discount and uses the same helper for its own lines:

class Receipt:
    def __init__(self):
        self._lines = []

    def _euros(self, cents):
        return f"{cents / 100:.2f} eur"

    def add(self, product, cents):
        self._lines.append((product, cents))

    def total(self):
        total = 0
        for product, cents in self._lines:
            total += cents
        return total

    def show(self):
        for product, cents in self._lines:
            print(product, self._euros(cents))
        print("Total", self._euros(self.total()))


class DiscountReceipt(Receipt):
    def __init__(self, percent):
        super().__init__()
        self._percent = percent

    def show(self):
        super().show()
        discount = self.total() * self._percent // 100
        print(f"Discount {self._percent}%", self._euros(discount))
        print("To pay", self._euros(self.total() - discount))


receipt = DiscountReceipt(10)
receipt.add("Bread", 349)
receipt.add("Cheese", 651)
receipt.show()
Output
Bread 3.49 eur
Cheese 6.51 eur
Total 10.00 eur
Discount 10% 1.00 eur
To pay 9.00 eur

The receipt's clients only need add, total and show. _euros is a tool for the receipt family, so it's protected. Had it been private, __euros, the call in DiscountReceipt would stop with an AttributeError, just as the private list of songs did. Notice also that DiscountReceipt stores its percentage in a protected attribute of its own, _percent, so that classes derived from it could use it too.

A protected method can be overridden like any other method. If a derived class wanted to show prices in another way, it could write its own _euros, and every line show prints would use it.

Choosing the level of access

  • Public, name: what clients use. Keep this small, and don't change it lightly, since other code depends on it.
  • Protected, _name: data and tools for the class and its derived classes.
  • Private, __name: details that only this one class handles, which not even a derived class should touch.

When you're unsure, start with less access. Opening something up later is easy, while taking access away breaks the code that already uses it.

Your task

Challenge 1 of 3

The method show of NameCard stops with an AttributeError, because it calls a private helper method of its base class. Make the helper method protected, so that the derived class can use it too. For example,

card = NameCard("Speaker", "Grace")
card.show()

should print:

| Speaker |
| Grace |

Challenge 2

The class Log keeps a program's log entries in a protected list, as pairs of a level and a text, and adds them with its protected method _write. Write a class AppLog that inherits Log, with these methods:

  • info(text) writes an entry with the level INFO.
  • error(text) writes an entry with the level ERROR.
  • error_count() returns how many entries have the level ERROR.

Add the entries with _write, and count them from _entries. For example,

log = AppLog()
log.info("Program started")
log.error("File not found")
log.info("Trying again")
log.error("Still not found")
log.show()
print(log.error_count())

should print:

[INFO] Program started
[ERROR] File not found
[INFO] Trying again
[ERROR] Still not found
2

Challenge 3

A Grades object keeps a student's grades in a protected list. Write a class CheckedGrades that inherits Grades and accepts only grades on a scale:

  • The constructor takes the scale's lowest and highest grade.
  • The protected method _check(grade) raises a ValueError with a message such as grade must be from 1 to 5 (with the scale's own numbers) when the grade is outside the scale. For a grade on the scale it does nothing.
  • add(grade) overrides Grades's: it checks the grade, and then adds it with super().
  • add_all(grades) adds a whole list of grades, but only if every one of them is on the scale. It checks them all first, so a list with even one bad grade adds none of them.

For example,

grades = CheckedGrades(1, 5)
grades.add(4)
grades.add_all([5, 3])
try:
    grades.add_all([2, 7, 4])
except ValueError as error:
    print("Error:", error)
try:
    grades.add(0)
except ValueError as error:
    print("Error:", error)
print(grades.all())
print(grades.average())

should print:

Error: grade must be from 1 to 5
Error: grade must be from 1 to 5
[4, 5, 3]
4.0