{"id":"access-modifiers-protected","slug":"protected","title":"Protected attributes","text":"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.\n\nPython 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.\n\nHere'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:\n\n```python\nclass Playlist:\n    def __init__(self, name):\n        self.name = name\n        self._songs = []\n\n    def add(self, title, seconds):\n        self._songs.append((title, seconds))\n\n    def count(self):\n        return len(self._songs)\n\n\nclass TimedPlaylist(Playlist):\n    def duration(self):\n        total = 0\n        for title, seconds in self._songs:\n            total += seconds\n        return f\"{total // 60} min {total % 60} s\"\n\n    def longest(self):\n        best_title = \"\"\n        best_seconds = 0\n        for title, seconds in self._songs:\n            if seconds > best_seconds:\n                best_title = title\n                best_seconds = seconds\n        return best_title\n\n\nmix = TimedPlaylist(\"Road trip\")\nmix.add(\"Highway Song\", 215)\nmix.add(\"Slow River\", 187)\nmix.add(\"River Bend\", 242)\nprint(mix.count())\nprint(mix.duration())\nprint(mix.longest())\n```\n\n```output\n3\n10 min 44 s\nRiver Bend\n```\n\n`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.\n\n### Only an agreement\n\nCode outside the classes could use a protected attribute too, and Python would let it. A private one is another matter:\n\n```pycon\n>>> class Note:\n...     def __init__(self):\n...         self._draft = \"buy milk\"\n...         self.__pin = \"1234\"\n...\n>>> note = Note()\n>>> note._draft\n'buy milk'\n>>> note.__pin\nTraceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\nAttributeError: 'Note' object has no attribute '__pin'\n```\n\nA 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.\n\n### Three levels of access\n\nPublic, protected and private are known as **access modifiers**: they set who may use an attribute or a method.\n\n| Access | Written as | The class itself | Derived classes | Clients |\n|---|---|---|---|---|\n| Public | `self.name` | yes | yes | yes |\n| Protected | `self._name` | yes | yes | no, by agreement |\n| Private | `self.__name` | yes | no | no |\n\nWhich 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.\n","task":"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:\n\n```\n['map', 'snacks', 'water']\n```\n","kind":"fill","seed":"class Backpack:\n    def __init__(self):\n        self._items = []\n\n    def pack(self, item):\n        self._items.append(item)\n\n\nclass SortedBackpack(Backpack):\n    def contents(self):\n        return sorted(self.[[_items]])\n\n\nif __name__ == \"__main__\":\n    bag = SortedBackpack()\n    bag.pack(\"water\")\n    bag.pack(\"map\")\n    bag.pack(\"snacks\")\n    print(bag.contents())\n","solution":"class Backpack:\n    def __init__(self):\n        self._items = []\n\n    def pack(self, item):\n        self._items.append(item)\n\n\nclass SortedBackpack(Backpack):\n    def contents(self):\n        return sorted(self._items)\n\n\nif __name__ == \"__main__\":\n    bag = SortedBackpack()\n    bag.pack(\"water\")\n    bag.pack(\"map\")\n    bag.pack(\"snacks\")\n    print(bag.contents())\n","hints":["The list is created in `Backpack`'s constructor. Use the same name here.","A protected name starts with one underscore, and Python doesn't rename it: the blank is `_items`."],"tests":[{"name":"Output","stdin":"","expect":"['map', 'snacks', 'water']","hidden":false}],"require":[],"forbid":[],"more":[{"task":"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,\n\n```python\nshop = ShopInventory()\nshop.add(\"apples\", 12)\nshop.add(\"pears\", 3)\nshop.add(\"plums\", 5)\nprint(shop.running_low(6))\nprint(shop.count(\"pears\"))\n```\n\nshould print:\n\n```\n['pears', 'plums']\n3\n```\n","kind":"code","seed":"class Inventory:\n    def __init__(self):\n        self.__stock = {}\n\n    def add(self, item, count):\n        if item not in self.__stock:\n            self.__stock[item] = 0\n        self.__stock[item] += count\n\n    def count(self, item):\n        if item not in self.__stock:\n            return 0\n        return self.__stock[item]\n\n\nclass ShopInventory(Inventory):\n    def running_low(self, limit):\n        low = []\n        for item in self.__stock:\n            if self.__stock[item] < limit:\n                low.append(item)\n        return low\n\n\nif __name__ == \"__main__\":\n    shop = ShopInventory()\n    shop.add(\"apples\", 12)\n    shop.add(\"pears\", 3)\n    print(shop.running_low(6))\n","solution":"class Inventory:\n    def __init__(self):\n        self._stock = {}\n\n    def add(self, item, count):\n        if item not in self._stock:\n            self._stock[item] = 0\n        self._stock[item] += count\n\n    def count(self, item):\n        if item not in self._stock:\n            return 0\n        return self._stock[item]\n\n\nclass ShopInventory(Inventory):\n    def running_low(self, limit):\n        low = []\n        for item in self._stock:\n            if self._stock[item] < limit:\n                low.append(item)\n        return low\n\n\nif __name__ == \"__main__\":\n    shop = ShopInventory()\n    shop.add(\"apples\", 12)\n    shop.add(\"pears\", 3)\n    print(shop.running_low(6))\n","hints":["A private name starts with two underscores and a protected one with one.","Change every `self.__stock` to `self._stock`, in both classes: the attribute must have the same name everywhere."],"tests":[{"name":"Test 1","stdin":"","call":"shop = ShopInventory()\nshop.add(\"apples\", 12)\nshop.add(\"pears\", 3)\nshop.add(\"plums\", 5)\nprint(shop.running_low(6))\nprint(shop.count(\"pears\"))\n","expect":"['pears', 'plums']\n3","hidden":false},{"name":"Test 2","stdin":"","call":"shop = ShopInventory()\nshop.add(\"rice\", 2)\nshop.add(\"beans\", 10)\nshop.add(\"rice\", 2)\nprint(shop.running_low(5))\nprint(shop.running_low(100))\nprint(shop.count(\"rice\"))\nprint(shop.count(\"salt\"))\n","expect":"['rice']\n['rice', 'beans']\n4\n0","hidden":true}],"require":[{"pattern":"self\\._stock\\b","message":"Make the stock protected: `self._stock`."}],"forbid":[{"pattern":"__stock","message":"Every `__stock` should become `_stock`, in both classes."}]},{"task":"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:\n\n- The constructor takes the course's name and `limit`, the most students the course can have.\n- `is_full()` returns `True` when the course has `limit` students.\n- `enroll(student)` overrides `Course`'s: when the course is full, it enrolls nobody and returns `False`. Otherwise it works as `Course`'s `enroll` does.\n\nFor example,\n\n```python\nyoga = LimitedCourse(\"Yoga\", 2)\nprint(yoga.enroll(\"Ada\"))\nprint(yoga.enroll(\"Ada\"))\nprint(yoga.enroll(\"Ben\"))\nprint(yoga.is_full())\nprint(yoga.enroll(\"Cy\"))\nprint(yoga)\n```\n\nshould print:\n\n```\nTrue\nFalse\nTrue\nTrue\nFalse\nYoga: 2 students\n```\n","kind":"code","seed":"class Course:\n    def __init__(self, name):\n        self.name = name\n        self._students = []\n\n    def enroll(self, student):\n        if student in self._students:\n            return False\n        self._students.append(student)\n        return True\n\n    def __str__(self):\n        return f\"{self.name}: {len(self._students)} students\"\n\n\n# Write the class LimitedCourse here.\n\n\nif __name__ == \"__main__\":\n    yoga = LimitedCourse(\"Yoga\", 2)\n    print(yoga.enroll(\"Ada\"))\n    print(yoga)\n","solution":"class Course:\n    def __init__(self, name):\n        self.name = name\n        self._students = []\n\n    def enroll(self, student):\n        if student in self._students:\n            return False\n        self._students.append(student)\n        return True\n\n    def __str__(self):\n        return f\"{self.name}: {len(self._students)} students\"\n\n\nclass LimitedCourse(Course):\n    def __init__(self, name, limit):\n        super().__init__(name)\n        self._limit = limit\n\n    def is_full(self):\n        return len(self._students) >= self._limit\n\n    def enroll(self, student):\n        if self.is_full():\n            return False\n        return super().enroll(student)\n\n\nif __name__ == \"__main__\":\n    yoga = LimitedCourse(\"Yoga\", 2)\n    print(yoga.enroll(\"Ada\"))\n    print(yoga)\n","hints":["The constructor calls `super().__init__(name)` and then stores the limit in an attribute of its own.","`is_full` compares the number of students, `len(self._students)`, with the limit.","`enroll` returns `False` if `self.is_full()`, and otherwise returns `super().enroll(student)`: the original still checks for a student who is already there."],"tests":[{"name":"Test 1","stdin":"","call":"yoga = LimitedCourse(\"Yoga\", 2)\nprint(yoga.enroll(\"Ada\"))\nprint(yoga.enroll(\"Ada\"))\nprint(yoga.enroll(\"Ben\"))\nprint(yoga.is_full())\nprint(yoga.enroll(\"Cy\"))\nprint(yoga)\n","expect":"True\nFalse\nTrue\nTrue\nFalse\nYoga: 2 students","hidden":false},{"name":"Test 2","stdin":"","call":"chess = LimitedCourse(\"Chess\", 3)\nprint(chess.is_full())\nfor name in [\"Kim\", \"Lee\", \"Kim\", \"Max\", \"Ola\", \"Pat\"]:\n    print(chess.enroll(name))\nprint(chess.is_full())\nprint(chess)\n","expect":"False\nTrue\nTrue\nFalse\nTrue\nFalse\nFalse\nTrue\nChess: 3 students","hidden":true}],"require":[{"pattern":"class\\s+LimitedCourse\\s*\\(\\s*Course\\s*\\)","message":"`LimitedCourse` should inherit `Course`."},{"pattern":"super\\(\\)\\.enroll\\(","message":"Let `Course`'s `enroll` do the enrolling: call it with `super().enroll(...)`."}],"forbid":[]}]}