{"id":"methods-encapsulation","slug":"keeping-it-valid","title":"Keeping the object valid","text":"The code that uses an object is called its **client**. The client asks the object to do things by calling its methods. That gives the methods a chance to make sure that the object's state always makes sense: a stock count is never negative, a volume knob stays between its lowest and highest setting.\n\nHere is a counter that counts down but never goes below zero, not even when it is made with a negative number:\n\n```python\nclass Countdown:\n    def __init__(self, start):\n        self.value = 0\n        if start > 0:\n            self.value = start\n\n    def decrease(self):\n        if self.value > 0:\n            self.value -= 1\n\n\ntimer = Countdown(2)\nfor i in range(4):\n    timer.decrease()\n    print(timer.value)\n\nbroken = Countdown(-5)\nprint(broken.value)\n```\n\n```output\n1\n0\n0\n0\n0\n```\n\nA method that can't do what was asked can also tell the client so, by returning `True` or `False`:\n\n```python\nclass Stock:\n    def __init__(self, item, amount):\n        self.item = item\n        self.amount = amount\n\n    def take(self, count):\n        if count <= self.amount:\n            self.amount -= count\n            return True\n        return False\n\n\npaint = Stock(\"paint can\", 5)\nprint(paint.take(3), paint.amount)\nprint(paint.take(3), paint.amount)\n```\n\n```output\nTrue 2\nFalse 2\n```\n\nThe second time, there aren't enough cans, so `take` changes nothing and returns `False`. The client can then decide what to do, such as print a message.\n\nWhen a request makes no sense at all, the method can raise an error, as you learned in part 6:\n\n```python\nclass Stock:\n    def __init__(self, item, amount):\n        self.item = item\n        self.amount = amount\n\n    def add(self, count):\n        if count < 0:\n            raise ValueError(\"count can't be negative\")\n        self.amount += count\n\n\npaint = Stock(\"paint can\", 5)\npaint.add(4)\nprint(paint.amount)\npaint.add(-10)\n```\n\n```output\n9\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 15, in <module>\n    paint.add(-10)\n    ~~~~~~~~~^^^^^\n  File \"main.py\", line 8, in add\n    raise ValueError(\"count can't be negative\")\nValueError: count can't be negative\n```\n\n### Encapsulation\n\nKeeping an object's data valid by handling it only through the object's methods is called **encapsulation**. The client calls `take` and `add` and doesn't need to know how the stock is kept inside the object.\n\nPython doesn't force this, though: a client can still write `paint.amount = -10`, and nothing stops it. The methods protect the object only when the client uses them. The Encapsulation module in part 9 shows how a class can hide its attributes; until then, change an object's state through its methods.\n","task":"The volume of this radio should stay between 0 and 10. Fill in the conditions, so that `up` and `down` never take it past those limits and the program prints:\n\n```\n10\n0\n```\n","kind":"fill","seed":"class Volume:\n    def __init__(self):\n        self.level = 5\n\n    def up(self):\n        if self.level [[< 10‖<= 9]]:\n            self.level += 1\n\n    def down(self):\n        if self.level [[> 0‖>= 1]]:\n            self.level -= 1\n\n\nradio = Volume()\nfor i in range(8):\n    radio.up()\nprint(radio.level)\nfor i in range(12):\n    radio.down()\nprint(radio.level)\n","solution":"class Volume:\n    def __init__(self):\n        self.level = 5\n\n    def up(self):\n        if self.level < 10:\n            self.level += 1\n\n    def down(self):\n        if self.level > 0:\n            self.level -= 1\n\n\nradio = Volume()\nfor i in range(8):\n    radio.up()\nprint(radio.level)\nfor i in range(12):\n    radio.down()\nprint(radio.level)\n","hints":["`up` may add one only while the level is below the highest setting, 10.","`down` may take one away only while the level is above 0: `> 0`."],"tests":[{"name":"Output","stdin":"","expect":"10\n0","hidden":false}],"require":[],"forbid":[],"more":[{"task":"Write a class `Battery`. A new battery has the attribute `charge` set to 100 (percent). Give it two methods:\n\n- `use(amount)` uses that much charge and returns `True`, when the battery has at least that much left. If it doesn't, the charge stays as it was and the method returns `False`.\n- `recharge(amount)` adds that much charge, but never more than up to 100.\n\nThis code:\n\n```python\nphone = Battery()\nprint(phone.use(70), phone.charge)\nprint(phone.use(40), phone.charge)\nphone.recharge(50)\nprint(phone.charge)\nphone.recharge(50)\nprint(phone.charge)\n```\n\nprints\n\n```\nTrue 30\nFalse 30\n80\n100\n```\n","kind":"code","seed":"class Battery:\n    def __init__(self):\n        self.charge = 100\n\n\nif __name__ == \"__main__\":\n    phone = Battery()\n    print(phone.use(70), phone.charge)\n","solution":"class Battery:\n    def __init__(self):\n        self.charge = 100\n\n    def use(self, amount):\n        if amount <= self.charge:\n            self.charge -= amount\n            return True\n        return False\n\n    def recharge(self, amount):\n        self.charge += amount\n        if self.charge > 100:\n            self.charge = 100\n\n\nif __name__ == \"__main__\":\n    phone = Battery()\n    print(phone.use(70), phone.charge)\n","hints":["In `use`, check first: `if amount <= self.charge:`. Only then take the amount away and return `True`. Otherwise return `False`.","In `recharge`, add the amount, then set the charge back to 100 if it went over."],"tests":[{"name":"Test 1","stdin":"","call":"phone = Battery()\nprint(phone.use(70), phone.charge)\nprint(phone.use(40), phone.charge)\nphone.recharge(50)\nprint(phone.charge)\nphone.recharge(50)\nprint(phone.charge)\n","expect":"True 30\nFalse 30\n80\n100","hidden":false},{"name":"Test 2","stdin":"","call":"b = Battery()\nprint(b.use(100), b.charge)\nprint(b.use(1), b.charge)\nb.recharge(250)\nprint(b.charge)\nprint(b.use(101), b.use(15), b.charge)\n","expect":"True 0\nFalse 0\n100\nFalse True 85","hidden":true}],"require":[],"forbid":[]},{"task":"Write a class `Thermostat` whose constructor takes the target temperature and stores it in the attribute `target`. When no temperature is given, it is 20. The target must always stay between 5 and 30 degrees, both included. Give the class these methods:\n\n- `warmer()` raises the target by one degree, but not above 30.\n- `cooler()` lowers the target by one degree, but not below 5.\n- `set(degrees)` sets the target to the given degrees. If they are below 5 or above 30, it changes nothing and raises a `ValueError` instead.\n\nThis code:\n\n```python\nhome = Thermostat()\nhome.warmer()\nprint(home.target)\nhome.set(30)\nhome.warmer()\nprint(home.target)\ntry:\n    home.set(45)\nexcept ValueError:\n    print(\"Not allowed\")\nprint(home.target)\n```\n\nprints\n\n```\n21\n30\nNot allowed\n30\n```\n","kind":"code","seed":"class Thermostat:\n    pass\n\n\nif __name__ == \"__main__\":\n    home = Thermostat()\n    home.warmer()\n    print(home.target)\n","solution":"class Thermostat:\n    def __init__(self, target=20):\n        self.target = target\n\n    def warmer(self):\n        if self.target < 30:\n            self.target += 1\n\n    def cooler(self):\n        if self.target > 5:\n            self.target -= 1\n\n    def set(self, degrees):\n        if degrees < 5 or degrees > 30:\n            raise ValueError(\"the target must be between 5 and 30\")\n        self.target = degrees\n\n\nif __name__ == \"__main__\":\n    home = Thermostat()\n    home.warmer()\n    print(home.target)\n","hints":["The constructor has a default value: `def __init__(self, target=20):`.","`warmer` adds one only `if self.target < 30`, and `cooler` takes one away only `if self.target > 5`.","In `set`, check the degrees first and `raise ValueError(\"...\")` when they're out of range. The line that changes the target comes after the check, so it never runs for a bad value."],"tests":[{"name":"Test 1","stdin":"","call":"home = Thermostat()\nhome.warmer()\nprint(home.target)\nhome.set(30)\nhome.warmer()\nprint(home.target)\ntry:\n    home.set(45)\nexcept ValueError:\n    print(\"Not allowed\")\nprint(home.target)\n","expect":"21\n30\nNot allowed\n30","hidden":false},{"name":"Test 2","stdin":"","call":"cabin = Thermostat(7)\nfor i in range(4):\n    cabin.cooler()\nprint(cabin.target)\ntry:\n    cabin.set(4)\nexcept ValueError:\n    print(\"Not allowed\")\nprint(cabin.target)\ncabin.set(5)\ncabin.warmer()\nprint(cabin.target)\n","expect":"5\nNot allowed\n5\n6","hidden":true},{"name":"Test 3","stdin":"","call":"t = Thermostat(29)\nt.warmer()\nt.warmer()\nt.cooler()\nprint(t.target)\nt.set(18)\nprint(t.target)\n","expect":"29\n18","hidden":true}],"require":[],"forbid":[]}]}