{"id":"class-attributes-example","slug":"classes-together","title":"Classes working together","text":"To finish this part, here is a larger program where several classes work together: a small city bike system. Read it slowly, class by class. Everything in it is something you've already learned; the point is how the pieces fit.\n\n- A `Bike` has a number. A class variable hands out the numbers, and the model has a default value.\n- A `Station` has a fixed number of slots and a private list of the `Bike` objects docked there. `release` returns a bike, or `None` when the station is empty.\n- A `Rider` has at most one bike at a time: the attribute `bike` is `None` while they walk.\n\n```python\nclass Bike:\n    next_number = 1\n\n    def __init__(self, model=\"city\"):\n        self.number = Bike.next_number\n        Bike.next_number += 1\n        self.model = model\n\n    def __str__(self):\n        return f\"bike {self.number} ({self.model})\"\n\n\nclass Station:\n    def __init__(self, name, slots):\n        self.__name = name\n        self.__slots = slots\n        self.__bikes = []\n\n    @property\n    def name(self):\n        return self.__name\n\n    def free_slots(self):\n        return self.__slots - len(self.__bikes)\n\n    def dock(self, bike):\n        if self.free_slots() == 0:\n            return False\n        self.__bikes.append(bike)\n        return True\n\n    def release(self):\n        if len(self.__bikes) == 0:\n            return None\n        return self.__bikes.pop(0)\n\n    def __str__(self):\n        return f\"{self.__name}: bikes {len(self.__bikes)}, free {self.free_slots()}\"\n\n\nclass Rider:\n    def __init__(self, name):\n        self.name = name\n        self.bike = None\n\n    def rent_from(self, station):\n        if self.bike is not None:\n            return\n        self.bike = station.release()\n        if self.bike is None:\n            print(f\"{self.name}: no bikes at {station.name}\")\n        else:\n            print(f\"{self.name} took {self.bike} at {station.name}\")\n\n    def return_to(self, station):\n        if self.bike is not None and station.dock(self.bike):\n            print(f\"{self.name} left {self.bike} at {station.name}\")\n            self.bike = None\n\n\npark = Station(\"Park\", 2)\nharbor = Station(\"Harbor\", 1)\npark.dock(Bike())\npark.dock(Bike(\"cargo\"))\nprint(park.dock(Bike()))\n\nada = Rider(\"Ada\")\nben = Rider(\"Ben\")\nada.rent_from(park)\nada.return_to(harbor)\nben.rent_from(harbor)\nada.rent_from(harbor)\nprint(park)\nprint(harbor)\n```\n\n```output\nFalse\nAda took bike 1 (city) at Park\nAda left bike 1 (city) at Harbor\nBen took bike 1 (city) at Harbor\nAda: no bikes at Harbor\nPark: bikes 1, free 1\nHarbor: bikes 0, free 1\n```\n\nSome things to notice:\n\n- **Each class looks after its own data.** The rider never touches the station's list; it asks the station with `station.release()` and `station.dock(...)`. So the station alone decides whether there's room, and its list can stay private.\n- **One object, many places.** Bike 1 is created once. It moves from the park's list to Ada, to the harbor's list, to Ben, but it's always the same object; only references to it move around.\n- **`None` means \"nothing here\".** `release` returns `None` from an empty station, and `rent_from` checks for it before printing. The third bike made, number 3, never found room, so it exists in no list at all.\n- **Methods take objects as arguments.** `rent_from(station)` works with any station, which is why one `Rider` class serves the whole city.\n","task":"The service van moves bikes from full stations to empty ones. Write the method `move_to(self, other, count)` in the class `Station`. It moves bikes one at a time from this station to the station `other`, until `count` bikes have moved, this station is out of bikes, or `other` is full. It returns the number of bikes it moved. Use `release` and `dock`.\n\nFor example, this code:\n\n```python\ndepot = Station(\"Depot\", 5)\ncorner = Station(\"Corner\", 2)\nfor i in range(4):\n    depot.dock(Bike())\nprint(depot.move_to(corner, 3))\nprint(depot)\nprint(corner)\n```\n\nprints:\n\n```\n2\nDepot: bikes 2, free 3\nCorner: bikes 2, free 0\n```\n","kind":"code","seed":"class Bike:\n    next_number = 1\n\n    def __init__(self, model=\"city\"):\n        self.number = Bike.next_number\n        Bike.next_number += 1\n        self.model = model\n\n    def __str__(self):\n        return f\"bike {self.number} ({self.model})\"\n\n\nclass Station:\n    def __init__(self, name, slots):\n        self.__name = name\n        self.__slots = slots\n        self.__bikes = []\n\n    @property\n    def name(self):\n        return self.__name\n\n    def free_slots(self):\n        return self.__slots - len(self.__bikes)\n\n    def dock(self, bike):\n        if self.free_slots() == 0:\n            return False\n        self.__bikes.append(bike)\n        return True\n\n    def release(self):\n        if len(self.__bikes) == 0:\n            return None\n        return self.__bikes.pop(0)\n\n    def move_to(self, other, count):\n        pass\n\n    def __str__(self):\n        return f\"{self.__name}: bikes {len(self.__bikes)}, free {self.free_slots()}\"\n\n\nif __name__ == \"__main__\":\n    depot = Station(\"Depot\", 5)\n    corner = Station(\"Corner\", 2)\n    for i in range(4):\n        depot.dock(Bike())\n    print(depot.move_to(corner, 3))\n    print(depot)\n    print(corner)\n","solution":"class Bike:\n    next_number = 1\n\n    def __init__(self, model=\"city\"):\n        self.number = Bike.next_number\n        Bike.next_number += 1\n        self.model = model\n\n    def __str__(self):\n        return f\"bike {self.number} ({self.model})\"\n\n\nclass Station:\n    def __init__(self, name, slots):\n        self.__name = name\n        self.__slots = slots\n        self.__bikes = []\n\n    @property\n    def name(self):\n        return self.__name\n\n    def free_slots(self):\n        return self.__slots - len(self.__bikes)\n\n    def dock(self, bike):\n        if self.free_slots() == 0:\n            return False\n        self.__bikes.append(bike)\n        return True\n\n    def release(self):\n        if len(self.__bikes) == 0:\n            return None\n        return self.__bikes.pop(0)\n\n    def move_to(self, other, count):\n        moved = 0\n        while moved < count and len(self.__bikes) > 0 and other.free_slots() > 0:\n            other.dock(self.release())\n            moved += 1\n        return moved\n\n    def __str__(self):\n        return f\"{self.__name}: bikes {len(self.__bikes)}, free {self.free_slots()}\"\n\n\nif __name__ == \"__main__\":\n    depot = Station(\"Depot\", 5)\n    corner = Station(\"Corner\", 2)\n    for i in range(4):\n        depot.dock(Bike())\n    print(depot.move_to(corner, 3))\n    print(depot)\n    print(corner)\n","hints":["Count the bikes moved in a local variable, and keep going with a `while` loop as long as all three conditions allow it.","Inside `move_to`, this station's list is `self.__bikes`, and you can ask the other station for its room with `other.free_slots()`.","One move is `other.dock(self.release())`: the bike leaves this station and docks at the other."],"tests":[{"name":"Test 1","stdin":"","call":"depot = Station(\"Depot\", 5)\ncorner = Station(\"Corner\", 2)\nfor i in range(4):\n    depot.dock(Bike())\nprint(depot.move_to(corner, 3))\nprint(depot)\nprint(corner)\n","expect":"2\nDepot: bikes 2, free 3\nCorner: bikes 2, free 0","hidden":false},{"name":"Test 2","stdin":"","call":"a = Station(\"A\", 3)\nb = Station(\"B\", 10)\nprint(a.move_to(b, 2))\na.dock(Bike())\na.dock(Bike())\na.dock(Bike())\nprint(a.move_to(b, 2))\nprint(a)\nprint(b)\nprint(a.move_to(b, 5))\nprint(a)\n","expect":"0\n2\nA: bikes 1, free 2\nB: bikes 2, free 8\n1\nA: bikes 0, free 3","hidden":true},{"name":"Test 3","stdin":"","call":"a = Station(\"A\", 4)\nb = Station(\"B\", 4)\nfor i in range(4):\n    a.dock(Bike())\nprint(a.move_to(b, 0))\nprint(a.move_to(b, 4))\nprint(a, \"|\", b)\nprint(b.release() is not None)\n","expect":"0\n4\nA: bikes 0, free 4 | B: bikes 4, free 0\nTrue","hidden":true}],"require":[],"forbid":[],"more":[{"task":"The city wants an overview of all its stations. Write the class `Network`:\n\n- `add(self, station)` adds a `Station` object to the network;\n- `total_bikes(self)` returns the number of bikes at all the stations together;\n- `busiest(self)` returns the station with the most bikes (the first one added, if several have the same number), or `None` if the network has no stations.\n\nThe class `Station` now has the method `bike_count()`, which returns the number of bikes docked at it. For example, this code:\n\n```python\ncity = Network()\nprint(city.busiest())\npark = Station(\"Park\", 4)\nharbor = Station(\"Harbor\", 4)\ncity.add(park)\ncity.add(harbor)\npark.dock(Bike())\nharbor.dock(Bike())\nharbor.dock(Bike())\nprint(city.total_bikes())\nprint(city.busiest())\n```\n\nprints:\n\n```\nNone\n3\nHarbor: bikes 2, free 2\n```\n","kind":"code","seed":"class Bike:\n    next_number = 1\n\n    def __init__(self, model=\"city\"):\n        self.number = Bike.next_number\n        Bike.next_number += 1\n        self.model = model\n\n\nclass Station:\n    def __init__(self, name, slots):\n        self.__name = name\n        self.__slots = slots\n        self.__bikes = []\n\n    def bike_count(self):\n        return len(self.__bikes)\n\n    def dock(self, bike):\n        if len(self.__bikes) == self.__slots:\n            return False\n        self.__bikes.append(bike)\n        return True\n\n    def __str__(self):\n        return f\"{self.__name}: bikes {len(self.__bikes)}, free {self.__slots - len(self.__bikes)}\"\n\n\nclass Network:\n    pass\n\n\nif __name__ == \"__main__\":\n    city = Network()\n    print(city.busiest())\n    park = Station(\"Park\", 4)\n    harbor = Station(\"Harbor\", 4)\n    city.add(park)\n    city.add(harbor)\n    park.dock(Bike())\n    harbor.dock(Bike())\n    harbor.dock(Bike())\n    print(city.total_bikes())\n    print(city.busiest())\n","solution":"class Bike:\n    next_number = 1\n\n    def __init__(self, model=\"city\"):\n        self.number = Bike.next_number\n        Bike.next_number += 1\n        self.model = model\n\n\nclass Station:\n    def __init__(self, name, slots):\n        self.__name = name\n        self.__slots = slots\n        self.__bikes = []\n\n    def bike_count(self):\n        return len(self.__bikes)\n\n    def dock(self, bike):\n        if len(self.__bikes) == self.__slots:\n            return False\n        self.__bikes.append(bike)\n        return True\n\n    def __str__(self):\n        return f\"{self.__name}: bikes {len(self.__bikes)}, free {self.__slots - len(self.__bikes)}\"\n\n\nclass Network:\n    def __init__(self):\n        self.__stations = []\n\n    def add(self, station):\n        self.__stations.append(station)\n\n    def total_bikes(self):\n        total = 0\n        for station in self.__stations:\n            total += station.bike_count()\n        return total\n\n    def busiest(self):\n        best = None\n        for station in self.__stations:\n            if best is None or station.bike_count() > best.bike_count():\n                best = station\n        return best\n\n\nif __name__ == \"__main__\":\n    city = Network()\n    print(city.busiest())\n    park = Station(\"Park\", 4)\n    harbor = Station(\"Harbor\", 4)\n    city.add(park)\n    city.add(harbor)\n    park.dock(Bike())\n    harbor.dock(Bike())\n    harbor.dock(Bike())\n    print(city.total_bikes())\n    print(city.busiest())\n","hints":["The network keeps its stations in a list, created empty in `__init__`.","`total_bikes` asks each station for its `bike_count()` and adds them up. The station's own list is private, so the network can't read it directly.","In `busiest`, start with `best = None`; take a station when `best is None` or when it has more bikes than `best`. An empty network then returns `None` by itself."],"tests":[{"name":"Test 1","stdin":"","call":"city = Network()\nprint(city.busiest())\npark = Station(\"Park\", 4)\nharbor = Station(\"Harbor\", 4)\ncity.add(park)\ncity.add(harbor)\npark.dock(Bike())\nharbor.dock(Bike())\nharbor.dock(Bike())\nprint(city.total_bikes())\nprint(city.busiest())\n","expect":"None\n3\nHarbor: bikes 2, free 2","hidden":false},{"name":"Test 2","stdin":"","call":"city = Network()\nprint(city.total_bikes())\na = Station(\"A\", 3)\nb = Station(\"B\", 3)\nc = Station(\"C\", 3)\nfor station in [a, b, c]:\n    city.add(station)\nprint(city.busiest())\nb.dock(Bike())\nb.dock(Bike())\nc.dock(Bike())\nc.dock(Bike())\nprint(city.busiest())\nprint(city.busiest() is b)\nprint(city.total_bikes())\n","expect":"0\nA: bikes 0, free 3\nB: bikes 2, free 1\nTrue\n4","hidden":true}],"require":[],"forbid":[]},{"task":"New bikes arrive in batches. Add the class method `batch(cls, count, model=\"city\")` to the class `Bike`. It creates `count` new bikes of the given model and returns them in a list. The bikes get their numbers from the class variable as usual.\n\nFor example, this code:\n\n```python\nfirst = Bike.batch(2)\nmore = Bike.batch(3, \"cargo\")\nprint(len(first), len(more))\nfor bike in more:\n    print(bike)\n```\n\nprints:\n\n```\n2 3\nbike 3 (cargo)\nbike 4 (cargo)\nbike 5 (cargo)\n```\n","kind":"code","seed":"class Bike:\n    next_number = 1\n\n    def __init__(self, model=\"city\"):\n        self.number = Bike.next_number\n        Bike.next_number += 1\n        self.model = model\n\n    def __str__(self):\n        return f\"bike {self.number} ({self.model})\"\n\n\nif __name__ == \"__main__\":\n    first = Bike.batch(2)\n    more = Bike.batch(3, \"cargo\")\n    print(len(first), len(more))\n    for bike in more:\n        print(bike)\n","solution":"class Bike:\n    next_number = 1\n\n    def __init__(self, model=\"city\"):\n        self.number = Bike.next_number\n        Bike.next_number += 1\n        self.model = model\n\n    @classmethod\n    def batch(cls, count, model=\"city\"):\n        bikes = []\n        for i in range(count):\n            bikes.append(cls(model))\n        return bikes\n\n    def __str__(self):\n        return f\"bike {self.number} ({self.model})\"\n\n\nif __name__ == \"__main__\":\n    first = Bike.batch(2)\n    more = Bike.batch(3, \"cargo\")\n    print(len(first), len(more))\n    for bike in more:\n        print(bike)\n","hints":["Put `@classmethod` above the method. Its parameters are `cls`, `count` and `model` with its default value.","Start with an empty list and loop `count` times with `range`.","Each round, `cls(model)` creates a new bike, which you append to the list. Return the list after the loop."],"tests":[{"name":"Test 1","stdin":"","call":"first = Bike.batch(2)\nmore = Bike.batch(3, \"cargo\")\nprint(len(first), len(more))\nfor bike in more:\n    print(bike)\n","expect":"2 3\nbike 3 (cargo)\nbike 4 (cargo)\nbike 5 (cargo)","hidden":false},{"name":"Test 2","stdin":"","call":"print(Bike.batch(0))\nBike(\"tandem\")\nfor bike in Bike.batch(2):\n    print(bike)\nprint(Bike.batch(1, \"electric\")[0].number)\nprint(Bike.next_number)\n","expect":"[]\nbike 2 (city)\nbike 3 (city)\n4\n5","hidden":true}],"require":[{"pattern":"@classmethod","raw":true,"message":"Make `batch` a class method, with `@classmethod` above it."}],"forbid":[]}]}