{"id":"encapsulation-private-methods","slug":"private-methods","title":"Private methods","text":"Two underscores at the start work for method names too. A private method is a **helper**: a piece of the class's own work that got a name of its own, to keep the other methods short or to save writing the same lines twice. The client never needs to call it.\n\nA taxi works out a fare in two places: when a customer asks what a ride would cost, and when the ride is driven. The formula is written once, in a private method:\n\n```python\nclass Taxi:\n    def __init__(self):\n        self.__earned = 0\n\n    def __fare(self, km):\n        return 4 + 2 * km\n\n    def quote(self, km):\n        print(f\"A {km} km ride costs {self.__fare(km)}\")\n\n    def drive(self, km):\n        self.__earned += self.__fare(km)\n\n    def earned(self):\n        return self.__earned\n\n\ntaxi = Taxi()\ntaxi.quote(10)\ntaxi.drive(10)\ntaxi.drive(3)\nprint(taxi.earned())\nprint(taxi.__fare(5))\n```\n\n```output\nA 10 km ride costs 24\n34\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 23, in <module>\n    print(taxi.__fare(5))\n          ^^^^^^^^^^^\nAttributeError: 'Taxi' object has no attribute '__fare'\n```\n\nInside the class, the helper is called like any other method, with `self.` in front: `self.__fare(km)`. Outside it, the name can't be reached, just as with a private attribute. If the price per kilometer changes, there is one line to fix, and no client has to change.\n\n### Public or private?\n\nAsk who the method is for. If it's something a client would want to do with the object, such as `quote`, `drive` or `earned`, make it public. If it only exists so that the class's own code is shorter or clearer, make it private. That keeps the class simple to use (the client sees only what it needs), and you can rename or rewrite a private helper later without breaking anyone's program.\n\nSome helpers are also unsafe in the client's hands, because they only make sense at certain moments. Here the scores are set up by a private method. The constructor uses it, and so does `new_game`, which first prints the final scores. If `__reset` were public, a client could wipe the scores in the middle of a game:\n\n```python\nclass Scoreboard:\n    def __init__(self, players):\n        self.__players = players\n        self.__reset()\n\n    def __reset(self):\n        self.__points = {}\n        for player in self.__players:\n            self.__points[player] = 0\n\n    def score(self, player, points):\n        self.__points[player] += points\n\n    def new_game(self):\n        print(\"Final scores:\", self.__points)\n        self.__reset()\n\n\nboard = Scoreboard([\"Ann\", \"Bo\"])\nboard.score(\"Ann\", 3)\nboard.score(\"Bo\", 5)\nboard.new_game()\nboard.score(\"Ann\", 1)\nboard.new_game()\n```\n\n```output\nFinal scores: {'Ann': 3, 'Bo': 5}\nFinal scores: {'Ann': 1, 'Bo': 0}\n```\n\n### Who sees what\n\nLook at the `Scoreboard` above from three places. Inside `score`, the names in reach are its parameters `player` and `points`, plus everything the object has: `self.__points`, `self.__players`, and `self.__reset()` too. The code at the bottom of the program can call `score` and `new_game`, but none of the names with two underscores. And the parameter `points` exists only while `score` runs. `new_game` can't see it, so any other method could have a variable called `points` of its own without a clash. (It has nothing to do with the attribute `self.__points` either: an attribute always comes with `self.` in front.)\n","task":"A door lock opens with a four-digit code. Fill in the blanks: the private method that checks a code is called `__correct`, and `open` uses it. The program should print:\n\n```\nWrong code\nDoor open\n```\n","kind":"fill","seed":"class Lock:\n    def __init__(self, code):\n        self.__code = code\n\n    def [[__correct]](self, attempt):\n        return attempt == self.__code\n\n    def open(self, attempt):\n        if self.[[__correct]](attempt):\n            print(\"Door open\")\n        else:\n            print(\"Wrong code\")\n\n\nlock = Lock(\"4711\")\nlock.open(\"1234\")\nlock.open(\"4711\")\n","solution":"class Lock:\n    def __init__(self, code):\n        self.__code = code\n\n    def __correct(self, attempt):\n        return attempt == self.__code\n\n    def open(self, attempt):\n        if self.__correct(attempt):\n            print(\"Door open\")\n        else:\n            print(\"Wrong code\")\n\n\nlock = Lock(\"4711\")\nlock.open(\"1234\")\nlock.open(\"4711\")\n","hints":["The two blanks are the same name, once where the method is defined and once where it's called.","The method's name is `__correct`, with two underscores, and inside the class it's called as `self.__correct(...)`."],"tests":[{"name":"Output","stdin":"","expect":"Wrong code\nDoor open","hidden":false}],"require":[],"forbid":[],"more":[{"task":"A shopping list should treat `\"Milk\"`, `\"milk\"` and `\" milk \"` as the same item. Write the class `ShoppingList` with a private list of items and these methods:\n\n- a private method `__clean(self, item)`, which returns the item with the spaces at both ends removed and in lowercase;\n- `add(self, item)`, which adds the cleaned item, unless it's already on the list;\n- `has(self, item)`, which returns `True` if the cleaned item is on the list;\n- `__str__(self)`, which returns the items separated by a comma and a space, in the order they were added.\n\nFor example, this code:\n\n```python\ngroceries = ShoppingList()\ngroceries.add(\"Milk\")\ngroceries.add(\"  eggs \")\ngroceries.add(\"MILK\")\nprint(groceries)\nprint(groceries.has(\" Eggs\"))\nprint(groceries.has(\"bread\"))\n```\n\nprints:\n\n```\nmilk, eggs\nTrue\nFalse\n```\n","kind":"code","seed":"class ShoppingList:\n    pass\n\n\nif __name__ == \"__main__\":\n    groceries = ShoppingList()\n    groceries.add(\"Milk\")\n    groceries.add(\"  eggs \")\n    groceries.add(\"MILK\")\n    print(groceries)\n    print(groceries.has(\" Eggs\"))\n    print(groceries.has(\"bread\"))\n","solution":"class ShoppingList:\n    def __init__(self):\n        self.__items = []\n\n    def __clean(self, item):\n        return item.strip().lower()\n\n    def add(self, item):\n        item = self.__clean(item)\n        if item not in self.__items:\n            self.__items.append(item)\n\n    def has(self, item):\n        return self.__clean(item) in self.__items\n\n    def __str__(self):\n        return \", \".join(self.__items)\n\n\nif __name__ == \"__main__\":\n    groceries = ShoppingList()\n    groceries.add(\"Milk\")\n    groceries.add(\"  eggs \")\n    groceries.add(\"MILK\")\n    print(groceries)\n    print(groceries.has(\" Eggs\"))\n    print(groceries.has(\"bread\"))\n","hints":["`__clean` can be one line with two string methods: `strip()` removes the spaces at both ends, `lower()` makes it lowercase.","Both `add` and `has` call `self.__clean(item)` first, then work with the cleaned item and the `in` operator.","`\", \".join(self.__items)` makes the string for `__str__`."],"tests":[{"name":"Test 1","stdin":"","call":"groceries = ShoppingList()\ngroceries.add(\"Milk\")\ngroceries.add(\"  eggs \")\ngroceries.add(\"MILK\")\nprint(groceries)\nprint(groceries.has(\" Eggs\"))\nprint(groceries.has(\"bread\"))\n","expect":"milk, eggs\nTrue\nFalse","hidden":false},{"name":"Test 2","stdin":"","call":"party = ShoppingList()\nprint(party.has(\"cake\"))\nfor item in [\"Cake \", \"juice\", \" CAKE\", \"Napkins\", \"juice\"]:\n    party.add(item)\nprint(party)\nprint(party.has(\"NAPKINS\"))\n","expect":"False\ncake, juice, napkins\nTrue","hidden":true}],"require":[{"pattern":"def\\s+__clean\\s*\\(","message":"Write the private helper method `__clean`."},{"pattern":"self\\.__clean\\s*\\(","min":2,"message":"Use `self.__clean(...)` in both `add` and `has`."}],"forbid":[]},{"task":"A robot moves on a board of 10 by 10 squares, where both `x` and `y` go from 0 to 9. It starts at (0, 0). Write the class `Robot` with:\n\n- a private method `__move(self, dx, dy)`, which moves the robot by `dx` steps along x and `dy` steps along y, but only if it stays on the board. Otherwise the robot stays where it is.\n- four public methods that use `__move`: `north` (y goes up by one), `south` (y goes down by one), `east` (x goes up by one) and `west` (x goes down by one).\n- `__str__`, which returns the position, such as `(2, 1)`.\n\nFor example, this code:\n\n```python\nrobot = Robot()\nrobot.west()\nrobot.east()\nrobot.east()\nrobot.north()\nprint(robot)\n```\n\nprints:\n\n```\n(2, 1)\n```\n","kind":"code","seed":"class Robot:\n    def __init__(self):\n        self.__x = 0\n        self.__y = 0\n\n    def __str__(self):\n        return f\"({self.__x}, {self.__y})\"\n\n\nif __name__ == \"__main__\":\n    robot = Robot()\n    robot.west()\n    robot.east()\n    robot.east()\n    robot.north()\n    print(robot)\n","solution":"class Robot:\n    def __init__(self):\n        self.__x = 0\n        self.__y = 0\n\n    def __move(self, dx, dy):\n        new_x = self.__x + dx\n        new_y = self.__y + dy\n        if 0 <= new_x <= 9 and 0 <= new_y <= 9:\n            self.__x = new_x\n            self.__y = new_y\n\n    def north(self):\n        self.__move(0, 1)\n\n    def south(self):\n        self.__move(0, -1)\n\n    def east(self):\n        self.__move(1, 0)\n\n    def west(self):\n        self.__move(-1, 0)\n\n    def __str__(self):\n        return f\"({self.__x}, {self.__y})\"\n\n\nif __name__ == \"__main__\":\n    robot = Robot()\n    robot.west()\n    robot.east()\n    robot.east()\n    robot.north()\n    print(robot)\n","hints":["In `__move`, work out the new position in two local variables first, and only store it if both coordinates are from 0 to 9.","Each public method is one line that calls the helper, for example `self.__move(0, 1)` for north.","West from (0, 0) would be x = -1, which is off the board, so the robot stays put."],"tests":[{"name":"Test 1","stdin":"","call":"robot = Robot()\nrobot.west()\nrobot.east()\nrobot.east()\nrobot.north()\nprint(robot)\n","expect":"(2, 1)","hidden":false},{"name":"Test 2","stdin":"","call":"robot = Robot()\nrobot.south()\nprint(robot)\nfor i in range(12):\n    robot.north()\n    robot.east()\nprint(robot)\nrobot.south()\nrobot.west()\nrobot.west()\nprint(robot)\n","expect":"(0, 0)\n(9, 9)\n(7, 8)","hidden":true}],"require":[{"pattern":"def\\s+__move\\s*\\(","message":"Write the private helper method `__move`."},{"pattern":"self\\.__move\\s*\\(","min":4,"message":"Let all four directions use `self.__move(...)`."}],"forbid":[]}]}