Python Arena
Step 4 of 4

Classes working together

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.

  • A Bike has a number. A class variable hands out the numbers, and the model has a default value.
  • 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.
  • A Rider has at most one bike at a time: the attribute bike is None while they walk.
class Bike:
    next_number = 1

    def __init__(self, model="city"):
        self.number = Bike.next_number
        Bike.next_number += 1
        self.model = model

    def __str__(self):
        return f"bike {self.number} ({self.model})"


class Station:
    def __init__(self, name, slots):
        self.__name = name
        self.__slots = slots
        self.__bikes = []

    @property
    def name(self):
        return self.__name

    def free_slots(self):
        return self.__slots - len(self.__bikes)

    def dock(self, bike):
        if self.free_slots() == 0:
            return False
        self.__bikes.append(bike)
        return True

    def release(self):
        if len(self.__bikes) == 0:
            return None
        return self.__bikes.pop(0)

    def __str__(self):
        return f"{self.__name}: bikes {len(self.__bikes)}, free {self.free_slots()}"


class Rider:
    def __init__(self, name):
        self.name = name
        self.bike = None

    def rent_from(self, station):
        if self.bike is not None:
            return
        self.bike = station.release()
        if self.bike is None:
            print(f"{self.name}: no bikes at {station.name}")
        else:
            print(f"{self.name} took {self.bike} at {station.name}")

    def return_to(self, station):
        if self.bike is not None and station.dock(self.bike):
            print(f"{self.name} left {self.bike} at {station.name}")
            self.bike = None


park = Station("Park", 2)
harbor = Station("Harbor", 1)
park.dock(Bike())
park.dock(Bike("cargo"))
print(park.dock(Bike()))

ada = Rider("Ada")
ben = Rider("Ben")
ada.rent_from(park)
ada.return_to(harbor)
ben.rent_from(harbor)
ada.rent_from(harbor)
print(park)
print(harbor)
Output
False
Ada took bike 1 (city) at Park
Ada left bike 1 (city) at Harbor
Ben took bike 1 (city) at Harbor
Ada: no bikes at Harbor
Park: bikes 1, free 1
Harbor: bikes 0, free 1

Some things to notice:

  • 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.
  • 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.
  • 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.
  • Methods take objects as arguments. rent_from(station) works with any station, which is why one Rider class serves the whole city.

Your task

Challenge 1 of 3

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.

For example, this code:

depot = Station("Depot", 5)
corner = Station("Corner", 2)
for i in range(4):
    depot.dock(Bike())
print(depot.move_to(corner, 3))
print(depot)
print(corner)

prints:

2
Depot: bikes 2, free 3
Corner: bikes 2, free 0

Challenge 2

The city wants an overview of all its stations. Write the class Network:

  • add(self, station) adds a Station object to the network;
  • total_bikes(self) returns the number of bikes at all the stations together;
  • 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.

The class Station now has the method bike_count(), which returns the number of bikes docked at it. For example, this code:

city = Network()
print(city.busiest())
park = Station("Park", 4)
harbor = Station("Harbor", 4)
city.add(park)
city.add(harbor)
park.dock(Bike())
harbor.dock(Bike())
harbor.dock(Bike())
print(city.total_bikes())
print(city.busiest())

prints:

None
3
Harbor: bikes 2, free 2

Challenge 3

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.

For example, this code:

first = Bike.batch(2)
more = Bike.batch(3, "cargo")
print(len(first), len(more))
for bike in more:
    print(bike)

prints:

2 3
bike 3 (cargo)
bike 4 (cargo)
bike 5 (cargo)