{"id":"access-modifiers-protected-methods","slug":"protected-methods","title":"Protected methods","text":"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.\n\nHere 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:\n\n```python\nclass Receipt:\n    def __init__(self):\n        self._lines = []\n\n    def _euros(self, cents):\n        return f\"{cents / 100:.2f} eur\"\n\n    def add(self, product, cents):\n        self._lines.append((product, cents))\n\n    def total(self):\n        total = 0\n        for product, cents in self._lines:\n            total += cents\n        return total\n\n    def show(self):\n        for product, cents in self._lines:\n            print(product, self._euros(cents))\n        print(\"Total\", self._euros(self.total()))\n\n\nclass DiscountReceipt(Receipt):\n    def __init__(self, percent):\n        super().__init__()\n        self._percent = percent\n\n    def show(self):\n        super().show()\n        discount = self.total() * self._percent // 100\n        print(f\"Discount {self._percent}%\", self._euros(discount))\n        print(\"To pay\", self._euros(self.total() - discount))\n\n\nreceipt = DiscountReceipt(10)\nreceipt.add(\"Bread\", 349)\nreceipt.add(\"Cheese\", 651)\nreceipt.show()\n```\n\n```output\nBread 3.49 eur\nCheese 6.51 eur\nTotal 10.00 eur\nDiscount 10% 1.00 eur\nTo pay 9.00 eur\n```\n\nThe 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.\n\nA 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.\n\n### Choosing the level of access\n\n- **Public**, `name`: what clients use. Keep this small, and don't change it lightly, since other code depends on it.\n- **Protected**, `_name`: data and tools for the class and its derived classes.\n- **Private**, `__name`: details that only this one class handles, which not even a derived class should touch.\n\nWhen you're unsure, start with less access. Opening something up later is easy, while taking access away breaks the code that already uses it.\n","task":"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,\n\n```python\ncard = NameCard(\"Speaker\", \"Grace\")\ncard.show()\n```\n\nshould print:\n\n```\n| Speaker |\n| Grace |\n```\n","kind":"code","seed":"class Card:\n    def __init__(self, title):\n        self.title = title\n\n    def __frame(self, text):\n        return \"| \" + text + \" |\"\n\n    def show(self):\n        print(self.__frame(self.title))\n\n\nclass NameCard(Card):\n    def __init__(self, title, name):\n        super().__init__(title)\n        self.name = name\n\n    def show(self):\n        super().show()\n        print(self.__frame(self.name))\n\n\nif __name__ == \"__main__\":\n    card = NameCard(\"Speaker\", \"Grace\")\n    card.show()\n","solution":"class Card:\n    def __init__(self, title):\n        self.title = title\n\n    def _frame(self, text):\n        return \"| \" + text + \" |\"\n\n    def show(self):\n        print(self._frame(self.title))\n\n\nclass NameCard(Card):\n    def __init__(self, title, name):\n        super().__init__(title)\n        self.name = name\n\n    def show(self):\n        super().show()\n        print(self._frame(self.name))\n\n\nif __name__ == \"__main__\":\n    card = NameCard(\"Speaker\", \"Grace\")\n    card.show()\n","hints":["A protected method's name starts with one underscore, a private one's with two.","Rename the method to `_frame`, and change both calls to `self._frame(...)`: the one in `Card` and the one in `NameCard`."],"tests":[{"name":"Test 1","stdin":"","call":"card = NameCard(\"Speaker\", \"Grace\")\ncard.show()\n","expect":"| Speaker |\n| Grace |","hidden":false},{"name":"Test 2","stdin":"","call":"card = NameCard(\"Guest\", \"Alan Turing\")\ncard.show()\nplain = Card(\"Welcome\")\nplain.show()\n","expect":"| Guest |\n| Alan Turing |\n| Welcome |","hidden":true}],"require":[{"pattern":"def\\s+_frame\\s*\\(","message":"Make the helper protected: `def _frame(self, text):`."},{"pattern":"self\\._frame\\(","min":2,"message":"Call the protected helper as `self._frame(...)` in both classes."}],"forbid":[{"pattern":"__frame","message":"Every `__frame` should become `_frame`."}],"more":[{"task":"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:\n\n- `info(text)` writes an entry with the level `INFO`.\n- `error(text)` writes an entry with the level `ERROR`.\n- `error_count()` returns how many entries have the level `ERROR`.\n\nAdd the entries with `_write`, and count them from `_entries`. For example,\n\n```python\nlog = AppLog()\nlog.info(\"Program started\")\nlog.error(\"File not found\")\nlog.info(\"Trying again\")\nlog.error(\"Still not found\")\nlog.show()\nprint(log.error_count())\n```\n\nshould print:\n\n```\n[INFO] Program started\n[ERROR] File not found\n[INFO] Trying again\n[ERROR] Still not found\n2\n```\n","kind":"code","seed":"class Log:\n    def __init__(self):\n        self._entries = []\n\n    def _write(self, level, text):\n        self._entries.append((level, text))\n\n    def show(self):\n        for level, text in self._entries:\n            print(f\"[{level}] {text}\")\n\n\n# Write the class AppLog here.\n\n\nif __name__ == \"__main__\":\n    log = AppLog()\n    log.info(\"Program started\")\n    log.error(\"File not found\")\n    log.show()\n    print(log.error_count())\n","solution":"class Log:\n    def __init__(self):\n        self._entries = []\n\n    def _write(self, level, text):\n        self._entries.append((level, text))\n\n    def show(self):\n        for level, text in self._entries:\n            print(f\"[{level}] {text}\")\n\n\nclass AppLog(Log):\n    def info(self, text):\n        self._write(\"INFO\", text)\n\n    def error(self, text):\n        self._write(\"ERROR\", text)\n\n    def error_count(self):\n        count = 0\n        for level, text in self._entries:\n            if level == \"ERROR\":\n                count += 1\n        return count\n\n\nif __name__ == \"__main__\":\n    log = AppLog()\n    log.info(\"Program started\")\n    log.error(\"File not found\")\n    log.show()\n    print(log.error_count())\n","hints":["`AppLog` needs no constructor: it inherits `Log`'s, which creates `self._entries`.","`info` and `error` are one line each: `self._write(\"INFO\", text)` and the same with `\"ERROR\"`.","Each entry is a tuple `(level, text)`. In `error_count`, loop with `for level, text in self._entries:` and count the entries whose level is `\"ERROR\"`."],"tests":[{"name":"Test 1","stdin":"","call":"log = AppLog()\nlog.info(\"Program started\")\nlog.error(\"File not found\")\nlog.info(\"Trying again\")\nlog.error(\"Still not found\")\nlog.show()\nprint(log.error_count())\n","expect":"[INFO] Program started\n[ERROR] File not found\n[INFO] Trying again\n[ERROR] Still not found\n2","hidden":false},{"name":"Test 2","stdin":"","call":"log = AppLog()\nprint(log.error_count())\nlog.info(\"Saved\")\nlog.info(\"Closed\")\nlog.show()\nprint(log.error_count())\nlog.error(\"Disk full\")\nprint(log.error_count())\n","expect":"0\n[INFO] Saved\n[INFO] Closed\n0\n1","hidden":true}],"require":[{"pattern":"class\\s+AppLog\\s*\\(\\s*Log\\s*\\)","message":"`AppLog` should inherit `Log`."},{"pattern":"self\\._write\\(","min":2,"message":"Add the entries with the protected helper: `self._write(...)`."},{"pattern":"_entries\\.append","max":1,"message":"Let `_write` add the entries; `AppLog` doesn't need to append to `_entries` itself."}],"forbid":[]},{"task":"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:\n\n- The constructor takes the scale's `lowest` and `highest` grade.\n- 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.\n- `add(grade)` overrides `Grades`'s: it checks the grade, and then adds it with `super()`.\n- `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.\n\nFor example,\n\n```python\ngrades = CheckedGrades(1, 5)\ngrades.add(4)\ngrades.add_all([5, 3])\ntry:\n    grades.add_all([2, 7, 4])\nexcept ValueError as error:\n    print(\"Error:\", error)\ntry:\n    grades.add(0)\nexcept ValueError as error:\n    print(\"Error:\", error)\nprint(grades.all())\nprint(grades.average())\n```\n\nshould print:\n\n```\nError: grade must be from 1 to 5\nError: grade must be from 1 to 5\n[4, 5, 3]\n4.0\n```\n","kind":"code","seed":"class Grades:\n    def __init__(self):\n        self._grades = []\n\n    def add(self, grade):\n        self._grades.append(grade)\n\n    def all(self):\n        return self._grades[:]\n\n    def average(self):\n        return sum(self._grades) / len(self._grades)\n\n\n# Write the class CheckedGrades here.\n\n\nif __name__ == \"__main__\":\n    grades = CheckedGrades(1, 5)\n    grades.add(4)\n    grades.add_all([5, 3])\n    print(grades.all())\n","solution":"class Grades:\n    def __init__(self):\n        self._grades = []\n\n    def add(self, grade):\n        self._grades.append(grade)\n\n    def all(self):\n        return self._grades[:]\n\n    def average(self):\n        return sum(self._grades) / len(self._grades)\n\n\nclass CheckedGrades(Grades):\n    def __init__(self, lowest, highest):\n        super().__init__()\n        self._lowest = lowest\n        self._highest = highest\n\n    def _check(self, grade):\n        if grade < self._lowest or grade > self._highest:\n            raise ValueError(f\"grade must be from {self._lowest} to {self._highest}\")\n\n    def add(self, grade):\n        self._check(grade)\n        super().add(grade)\n\n    def add_all(self, grades):\n        for grade in grades:\n            self._check(grade)\n        for grade in grades:\n            super().add(grade)\n\n\nif __name__ == \"__main__\":\n    grades = CheckedGrades(1, 5)\n    grades.add(4)\n    grades.add_all([5, 3])\n    print(grades.all())\n","hints":["The constructor calls `super().__init__()`, which creates the list of grades, and then stores the lowest and the highest grade in attributes of its own.","`_check` raises the error when `grade < self._lowest or grade > self._highest`: `raise ValueError(f\"grade must be from {self._lowest} to {self._highest}\")`.","`add` calls `self._check(grade)` and then `super().add(grade)`: if the check raises, the method stops before anything is added. `add_all` needs two loops: one that checks every grade, and only after it one that adds them."],"tests":[{"name":"Test 1","stdin":"","call":"grades = CheckedGrades(1, 5)\ngrades.add(4)\ngrades.add_all([5, 3])\ntry:\n    grades.add_all([2, 7, 4])\nexcept ValueError as error:\n    print(\"Error:\", error)\ntry:\n    grades.add(0)\nexcept ValueError as error:\n    print(\"Error:\", error)\nprint(grades.all())\nprint(grades.average())\n","expect":"Error: grade must be from 1 to 5\nError: grade must be from 1 to 5\n[4, 5, 3]\n4.0","hidden":false},{"name":"Test 2","stdin":"","call":"grades = CheckedGrades(4, 10)\ngrades.add_all([10, 4])\ntry:\n    grades.add(3)\nexcept ValueError as error:\n    print(\"Error:\", error)\ntry:\n    grades.add_all([7, 8, 11])\nexcept ValueError as error:\n    print(\"Error:\", error)\ngrades.add(9)\nprint(grades.all())\nprint(grades.average())\n","expect":"Error: grade must be from 4 to 10\nError: grade must be from 4 to 10\n[10, 4, 9]\n7.666666666666667","hidden":true},{"name":"Test 3","stdin":"","call":"grades = CheckedGrades(0, 100)\ngrades.add_all([])\ngrades.add(100)\ngrades.add(0)\ntry:\n    grades.add(-1)\nexcept ValueError as error:\n    print(\"Error:\", error)\nprint(grades.all())\n","expect":"Error: grade must be from 0 to 100\n[100, 0]","hidden":true}],"require":[{"pattern":"class\\s+CheckedGrades\\s*\\(\\s*Grades\\s*\\)","message":"`CheckedGrades` should inherit `Grades`."},{"pattern":"def\\s+_check\\s*\\(","message":"Write the check in a protected method `_check(grade)`."},{"pattern":"self\\._check\\(","min":2,"message":"Both `add` and `add_all` should check the grades with `self._check(...)`."},{"pattern":"super\\(\\)\\.add\\(","message":"Let `Grades`'s `add` add a grade: `super().add(...)`."}],"forbid":[]}]}