{"id":"methods-helpers","slug":"helper-methods","title":"Helper methods and local variables","text":"As a class grows, some of its methods are there only to help the others. A **helper method** is called through `self`, from the constructor or from another method. This booking class checks its arguments with one:\n\n```python\nclass Booking:\n    def __init__(self, name, guests, nights):\n        self.name = name\n        self.guests = 1\n        self.nights = 1\n        if self.in_range(guests, 8):\n            self.guests = guests\n        if self.in_range(nights, 30):\n            self.nights = nights\n\n    def in_range(self, number, largest):\n        return 1 <= number <= largest\n\n    def price(self):\n        per_night = 50 + 20 * (self.guests - 1)\n        return per_night * self.nights\n\n\nkim = Booking(\"Kim\", 3, 2)\nlee = Booking(\"Lee\", 12, 0)\nprint(kim.guests, kim.nights, kim.price())\nprint(lee.guests, lee.nights, lee.price())\n```\n\n```output\n3 2 180\n1 1 50\n```\n\nThe constructor first gives every attribute a safe value, then uses `self.in_range` to decide whether the arguments can replace them. Lee's booking had impossible numbers, so it keeps the safe ones. Forget the `self.` in front of a helper method, and Python stops with `NameError: name 'in_range' is not defined. Did you mean: 'self.in_range'?`\n\n### Local variables\n\nLook at `per_night` in `price`. It has no `self.` in front, so it is an ordinary **local variable**: it exists only while the method runs, like a variable inside any function. Use attributes for what the object must remember from one call to the next, and local variables for the steps of a calculation.\n\n### What an object needs to remember\n\nAn object doesn't have to keep a record of everything that happens to it, only what its methods need. A habit tracker that reports your current streak (how many days in a row you have practiced, up to the last one) and your longest streak needs two numbers, not a list of every day:\n\n```python\nclass Streak:\n    def __init__(self):\n        self.current = 0\n        self.longest = 0\n\n    def log(self, done):\n        if done:\n            self.current += 1\n            if self.current > self.longest:\n                self.longest = self.current\n        else:\n            self.current = 0\n\n\nguitar = Streak()\nwhile True:\n    answer = input(\"Practiced today? \")\n    if answer == \"\":\n        break\n    guitar.log(answer == \"yes\")\n\nprint(\"Current streak:\", guitar.current)\nprint(\"Longest streak:\", guitar.longest)\n```\n\n```input\nyes\nyes\nyes\nno\nyes\n\n```\n\n```output\nPracticed today? yes\nPracticed today? yes\nPracticed today? yes\nPracticed today? no\nPracticed today? yes\nPracticed today?\nCurrent streak: 1\nLongest streak: 3\n```\n\nEach call of `log` keeps both numbers up to date: a day of practice makes the current streak one longer, and the longest one too if it was just beaten, and a day off starts the current streak again from 0. The main program only reads the answers and hands each one to the object. `answer == \"yes\"` is already `True` or `False`, just what `log` expects.\n","task":"This class has two mistakes, both in how it calls its own methods. Fix them, so that the program prints:\n\n```\nAnn 42 70 True\nBo 0 0 False\n```\n","kind":"code","seed":"class Exam:\n    def __init__(self, student, points):\n        self.student = student\n        self.points = 0\n        if valid(points):\n            self.points = points\n\n    def valid(self, points):\n        return 0 <= points <= 60\n\n    def percent(self):\n        share = self.points / 60 * 100\n        return round(share)\n\n    def passed(self):\n        return percent() >= 50\n\n\nann = Exam(\"Ann\", 42)\nbo = Exam(\"Bo\", 75)\nprint(ann.student, ann.points, ann.percent(), ann.passed())\nprint(bo.student, bo.points, bo.percent(), bo.passed())\n","solution":"class Exam:\n    def __init__(self, student, points):\n        self.student = student\n        self.points = 0\n        if self.valid(points):\n            self.points = points\n\n    def valid(self, points):\n        return 0 <= points <= 60\n\n    def percent(self):\n        share = self.points / 60 * 100\n        return round(share)\n\n    def passed(self):\n        return self.percent() >= 50\n\n\nann = Exam(\"Ann\", 42)\nbo = Exam(\"Bo\", 75)\nprint(ann.student, ann.points, ann.percent(), ann.passed())\nprint(bo.student, bo.points, bo.percent(), bo.passed())\n","hints":["Check it and read the error. Python even suggests the fix.","A method of the same object is called through `self`: `self.valid(points)`.","The second mistake is in `passed`, which calls `percent` the same wrong way."],"tests":[{"name":"Output","stdin":"","expect":"Ann 42 70 True\nBo 0 0 False","hidden":false}],"require":[],"forbid":[],"more":[{"task":"Write a class `Expenses` that keeps track of money spent. Give it these methods:\n\n- `add(amount)` records one expense.\n- `count()` returns how many expenses have been recorded.\n- `total()` returns their sum.\n- `largest()` returns the largest expense, or 0 when there are none.\n\nDecide yourself which attributes the class needs, but don't give an attribute the same name as a method: `self.count = 0` would hide the method `count`. This code:\n\n```python\ntrip = Expenses()\ntrip.add(12.5)\ntrip.add(40)\ntrip.add(7.5)\nprint(trip.count(), trip.total(), trip.largest())\n```\n\nprints\n\n```\n3 60.0 40\n```\n","kind":"code","seed":"class Expenses:\n    pass\n\n\nif __name__ == \"__main__\":\n    trip = Expenses()\n    trip.add(12.5)\n    trip.add(40)\n    print(trip.count(), trip.total(), trip.largest())\n","solution":"class Expenses:\n    def __init__(self):\n        self.number = 0\n        self.sum = 0\n        self.biggest = 0\n\n    def add(self, amount):\n        self.number += 1\n        self.sum += amount\n        if amount > self.biggest:\n            self.biggest = amount\n\n    def count(self):\n        return self.number\n\n    def total(self):\n        return self.sum\n\n    def largest(self):\n        return self.biggest\n\n\nif __name__ == \"__main__\":\n    trip = Expenses()\n    trip.add(12.5)\n    trip.add(40)\n    print(trip.count(), trip.total(), trip.largest())\n","hints":["The constructor sets up what the object must remember: how many expenses, their sum and the largest so far, all 0 at the start.","`add` updates all three. The largest changes only when the new amount is bigger than the largest so far.","An attribute can't have the same name as a method of the class: `self.count = 0` would hide the method `count`. Name the attributes something else, such as `self.number`."],"tests":[{"name":"Test 1","stdin":"","call":"trip = Expenses()\ntrip.add(12.5)\ntrip.add(40)\ntrip.add(7.5)\nprint(trip.count(), trip.total(), trip.largest())\n","expect":"3 60.0 40","hidden":false},{"name":"Test 2","stdin":"","call":"empty = Expenses()\nprint(empty.count(), empty.total(), empty.largest())\nweek = Expenses()\nfor amount in [3, 18, 2, 18, 9]:\n    week.add(amount)\nprint(week.count(), week.total(), week.largest())\nprint(empty.count())\n","expect":"0 0 0\n5 50 18\n0","hidden":true}],"require":[],"forbid":[]},{"task":"Write a class `Elevator` for a building with the floors 0 to 10. An elevator remembers just two numbers: the floor it is on, in the attribute `floor`, and how many floors it has traveled in all, in the attribute `traveled`. A new elevator is on floor 0 and has traveled 0 floors. Give the class two methods:\n\n- `exists(floor)`, a helper method, returns `True` when the building has that floor.\n- `go_to(floor)` moves the elevator to the floor, adds the number of floors it passes to `traveled` and returns `True`. From floor 2 up to 7 is 5 floors, and from 7 down to 3 is 4. For a floor that doesn't exist, it changes nothing and returns `False`: use `exists` to find out.\n\nThen write the main program. It asks for floors until the user types an empty line, prints `No such floor` when the elevator can't go to the floor, and at the end tells where the elevator is and how far it went, as in this example:\n\n```\nFloor: 4\nFloor: 12\nNo such floor\nFloor: 1\nFloor:\nNow on floor 1, traveled 7 floors\n```\n","kind":"code","seed":"class Elevator:\n    def __init__(self):\n        self.floor = 0\n        self.traveled = 0\n\n    # Write the methods here.\n\n\n# Write the main program here.\n","solution":"class Elevator:\n    def __init__(self):\n        self.floor = 0\n        self.traveled = 0\n\n    def exists(self, floor):\n        return 0 <= floor <= 10\n\n    def go_to(self, floor):\n        if not self.exists(floor):\n            return False\n        self.traveled += abs(floor - self.floor)\n        self.floor = floor\n        return True\n\n\nlift = Elevator()\nwhile True:\n    line = input(\"Floor: \")\n    if line == \"\":\n        break\n    if not lift.go_to(int(line)):\n        print(\"No such floor\")\n\nprint(f\"Now on floor {lift.floor}, traveled {lift.traveled} floors\")\n","hints":["`exists` can return a comparison: `0 <= floor <= 10`. In `go_to`, check `if not self.exists(floor):` first, and return `False` before anything changes.","The floors passed are the difference between the new floor and the old one, without its minus sign: `abs(floor - self.floor)`. Add it to `self.traveled` before `self.floor` changes.","In the main program, make one `Elevator()` before the loop. `go_to` tells you whether the move worked, so print the message when it returns `False`: `if not lift.go_to(int(line)):`."],"tests":[{"name":"Test 1","stdin":"4\n12\n1\n\n","expect":"Floor: 4\nFloor: 12\nNo such floor\nFloor: 1\nFloor:\nNow on floor 1, traveled 7 floors","hidden":false},{"name":"Test 2","stdin":"10\n-1\n0\n10\n11\n5\n\n","expect":"Floor: 10\nFloor: -1\nNo such floor\nFloor: 0\nFloor: 10\nFloor: 11\nNo such floor\nFloor: 5\nFloor:\nNow on floor 5, traveled 35 floors","hidden":true},{"name":"Test 3","stdin":"3\n3\n7\n\n","expect":"Floor: 3\nFloor: 3\nFloor: 7\nFloor:\nNow on floor 7, traveled 7 floors","hidden":true},{"name":"Test 4","stdin":"\n","expect":"Floor:\nNow on floor 0, traveled 0 floors","hidden":true}],"require":[{"pattern":"self\\.exists\\(","message":"Use the helper method in `go_to`: call it with `self.exists(floor)`."},{"pattern":"Elevator\\(\\)","message":"Make an `Elevator` object in the main program with `Elevator()`."}],"forbid":[]}]}