SpiralTrain
Exercises › Block 1 · Exercise 4

Block 1 · Exercise 4

Classes

Starter notebook
04-classes-starter
Fabric path
/lakehouse/default/Files/data/solutions/04.classes/

Open the notebook 04-classes-starter in your workspace. It holds car.py, from /lakehouse/default/Files/data/solutions/04.classes/starter/, in one cell : a Car class with validated model, plate and price properties, a show_statistics function, and at the bottom a fleet of three cars that it prints and reports on. Run it and read the statistics.

Grow that one cell, one part at a time, 1a, 1b and so on, and compare with the expected output before you go on. Put new classes above Car, and the calls that test a part at the bottom of the cell. The solution is in 04-classes-solution, under every part on the exercise site, and at the back of the exercises PDF.

Step 1Tank and Dashboard

Give the Car two parts : a Tank that holds the fuel level and a Dashboard that reads the level back.

1a

Add a Tank class with a minimum level, a maximum level and a current level. The minimum defaults to 5, the maximum to 20, and the current level starts at the minimum. Create a Tank() and print its current_level.

Hint 1

The slide The init Initializer shows how default arguments and instance attributes go together.

Check your output
5
Show solutionHide solution
python
class Tank:
    def __init__(self, min_level: int = 5, max_level: int = 20) -> None:
        self.min_level = min_level
        self.max_level = max_level
        self.current_level = min_level


print(Tank().current_level)

1b

Add a Dashboard class. It is handed the car when it is created, and keeps it on the instance together with a speed that starts at 0. Give it a read-only property fuel_indicator that returns the level of the car's tank.

Hint 1

The slide Properties shows how to make a value that is read but not set.

Hint 2

The dashboard reads the level through the car it was given : self.car.tank.current_level.

Show solutionHide solution
python
class Dashboard:
    def __init__(self, car: "Car") -> None:
        self.car = car
        self.speed = 0

    @property
    def fuel_indicator(self) -> int:
        return self.car.tank.current_level

1c

Give the Car a tank and a dashboard in its __init__. Then make a golf = Car("Golf", "AA-11-BB", 25000) at the bottom of the cell and print golf.dashboard.fuel_indicator.

Hint 1

The dashboard needs the car it reads from, so the car has to hand itself over. The slide Building from Parts shows a part that gets its owner.

Check your output
5
Show solutionHide solution
python
        self.tank = Tank()
        self.dashboard = Dashboard(self)
python
golf = Car("Golf", "AA-11-BB", 25000)
print(golf.dashboard.fuel_indicator)

1d

Add a refuel method to Car that fills the tank one litre at a time until it reaches the maximum level. Print a line at each step with the licence plate and the current level, read from the dashboard. Call golf.refuel().

Hint 1

Loop while the level is below the maximum, and add one to it on every round.

Check your output
Refuelling AA-11-BB : level = 6
Refuelling AA-11-BB : level = 7
...
Refuelling AA-11-BB : level = 20

The lines in between count up one by one. Your wording may differ.

Show solutionHide solution
python
    def refuel(self) -> None:
        while self.tank.current_level < self.tank.max_level:
            self.tank.current_level += 1
            print(f"Refuelling {self.plate} : level = {self.dashboard.fuel_indicator}")


golf.refuel()

1e

Add a drive method that lowers the level one litre at a time until the tank is empty, and prints the plate and the level at each step. Once the level is at or below the minimum, it also prints a warning. It refuses to drive when the tank is already empty. Call golf.drive().

Hint 1

The tank is empty when its level reaches 0. Check for that before the loop starts.

Check your output
Driving AA-11-BB : level = 19
Driving AA-11-BB : level = 18
...
Driving AA-11-BB : level = 5
WARNING : running out of fuel
Driving AA-11-BB : level = 4
WARNING : running out of fuel
...
Driving AA-11-BB : level = 0
WARNING : running out of fuel

The warning appears from level 5 down. Your wording may differ.

Show solutionHide solution
python
    def drive(self) -> None:
        if self.tank.current_level <= 0:
            print(f"{self.plate} cannot drive : the tank is empty.")
            return
        while self.tank.current_level > 0:
            self.tank.current_level -= 1
            print(f"Driving {self.plate} : level = {self.dashboard.fuel_indicator}")
            if self.tank.current_level <= self.tank.min_level:
                print("WARNING : running out of fuel")


golf.drive()

1f

Call golf.drive() a second time. Write a comment that says what it prints and why.

Hint 1

What is the level of the tank when the first drive() has finished?

Check your output
AA-11-BB cannot drive : the tank is empty.
Show solutionHide solution
python
golf.drive()
# The first drive() ran until the level reached 0, so the tank is empty.
# The second call sees that at the top and prints the refusal without driving.

Step 2Car family

Every car runs, but a fuel car and an electric car cost different amounts to keep on the road.

2a

Give the base Car a method running_cost that returns the yearly cost of running it. For a plain car this is 0.0. Print golf.running_cost().

Check your output
0.0
Show solutionHide solution
python
    def running_cost(self) -> float:
        return 0.0


print(golf.running_cost())

2b

Derive a class FuelCar from Car. Its initializer takes model, plate, price, fuel_price, consumption and annual_km, passes the first three to the base initializer and stores the rest. Its running_cost is the yearly fuel cost : fuel_price * consumption / 100 * annual_km. Print the running cost of FuelCar("Golf", "AA-11-BB", 25000, 1.85, 6.5, 15000).

Hint 1

The slide Inheritance shows how a derived class calls the base initializer with super().__init__(...).

Hint 2

The slide Overriding and Polymorphism shows a method that a derived class replaces.

Check your output
1803.75
Show solutionHide solution
python
class FuelCar(Car):
    def __init__(self, model: str, plate: str, price: float, fuel_price: float,
                 consumption: float, annual_km: float) -> None:
        super().__init__(model, plate, price)
        self.fuel_price = fuel_price
        self.consumption = consumption
        self.annual_km = annual_km

    def running_cost(self) -> float:
        return self.fuel_price * self.consumption / 100 * self.annual_km


print(FuelCar("Golf", "AA-11-BB", 25000, 1.85, 6.5, 15000).running_cost())

2c

Derive an ElectricCar whose running cost is a fixed yearly charging fee. The fee is a parameter annual_fee that defaults to 500.0. Print the running cost of ElectricCar("Model 3", "CC-22-DD", 42000).

Check your output
500.0
Show solutionHide solution
python
class ElectricCar(Car):
    def __init__(self, model: str, plate: str, price: float,
                 annual_fee: float = 500.0) -> None:
        super().__init__(model, plate, price)
        self.annual_fee = annual_fee

    def running_cost(self) -> float:
        return self.annual_fee


print(ElectricCar("Model 3", "CC-22-DD", 42000).running_cost())

2d

Give Car a method describe that returns a short sentence about what the car is, and override it in FuelCar and in ElectricCar. Print the describe() of an instance of each of the three classes.

Check your output
a car
a fuel car for everyday driving
an electric car with a fixed charging fee

Your sentences may differ.

Show solutionHide solution
python
    # in class Car
    def describe(self) -> str:
        return "a car"

    # in class FuelCar
    def describe(self) -> str:
        return "a fuel car for everyday driving"

    # in class ElectricCar
    def describe(self) -> str:
        return "an electric car with a fixed charging fee"


print(golf.describe())
print(FuelCar("Golf", "AA-11-BB", 25000, 1.85, 6.5, 15000).describe())
print(ElectricCar("Model 3", "CC-22-DD", 42000).describe())

2e

Extend show_statistics so that it also prints the total running cost of the fleet, and one line per car with its plate, its describe() and its running_cost(). Replace the fleet at the bottom of the cell, the loop that follows it and the report call with this, and report on it :

python
fleet: list[Car] = [
    FuelCar("Golf", "AA-11-BB", 25000, 1.85, 6.5, 15000),
    ElectricCar("Model 3", "CC-22-DD", 42000),
    FuelCar("Clio", "EE-33-FF", 18000, 1.85, 5.2, 12000),
]
show_statistics(fleet)
Hint 1

Sum car.running_cost() over the fleet the way the function already sums car.price.

Check your output
Cars in fleet      : 3
Total value        : 85000.00
Average price      : 28333.33
Most expensive     : Model 3 (CC-22-DD)
Total running cost : 3458.15
  AA-11-BB : a fuel car for everyday driving (running cost 1803.75)
  CC-22-DD : an electric car with a fixed charging fee (running cost 500.00)
  EE-33-FF : a fuel car for everyday driving (running cost 1154.40)

Your layout and sentences may differ. The numbers have to match.

Show solutionHide solution
python
def show_statistics(fleet: list[Car]) -> None:
    total = sum(car.price for car in fleet)
    running = sum(car.running_cost() for car in fleet)
    print(f"Cars in fleet      : {len(fleet)}")
    print(f"Total value        : {total:.2f}")
    print(f"Average price      : {total / len(fleet):.2f}")
    dearest = max(fleet, key=lambda car: car.price)
    print(f"Most expensive     : {dearest.model} ({dearest.plate})")
    print(f"Total running cost : {running:.2f}")
    for car in fleet:
        print(f"  {car.plate} : {car.describe()} (running cost {car.running_cost():.2f})")

Step 3Duck typing

3a

Write a class Trailer that is not a Car, and give it everything show_statistics reads from a car : a model, a plate, a price, a describe() and a running_cost(). It takes a plate, and sets the rest itself. Add one to the fleet and report again :

python
fleet.append(Trailer("WX-99-YZ"))
show_statistics(fleet)
Hint 1

Read show_statistics and list every attribute and method it touches on a car. The trailer needs each one, and needs no base class.

Check your output
Cars in fleet      : 4
Total value        : 87500.00
Average price      : 21875.00
Most expensive     : Model 3 (CC-22-DD)
Total running cost : 3578.15
  AA-11-BB : a fuel car for everyday driving (running cost 1803.75)
  CC-22-DD : an electric car with a fixed charging fee (running cost 500.00)
  EE-33-FF : a fuel car for everyday driving (running cost 1154.40)
  WX-99-YZ : a trailer, which is not a car (running cost 120.00)

The trailer's line and the totals depend on the values you gave it.

Show solutionHide solution
python
class Trailer:
    def __init__(self, plate: str, price: float = 2500.0) -> None:
        self.plate = plate
        self.model = "trailer"
        self.price = price

    def running_cost(self) -> float:
        return 120.0

    def describe(self) -> str:
        return "a trailer, which is not a car"

3b

Leave describe() out of Trailer and run the cell again. Write a comment that says which line fails, and at what point in the report.

Hint 1

The loop that prints a line per car asks each object for describe() in turn.

Check your output
AttributeError: 'Trailer' object has no attribute 'describe'
Show solutionHide solution
python
# The line in show_statistics that calls car.describe() fails, with an AttributeError.
# It fails only when the loop reaches the trailer : the totals above it have already printed.

If time permits

  • Add a RaceCar derived from FuelCar. It takes races, cost_per_race and prize_money after the arguments of a FuelCar. Its running cost is the fuel cost, plus for each race the cost per race, minus the prize money. Reuse the fuel cost with super().running_cost(), and add one to the fleet :

    python
    RaceCar("F40", "GG-44-HH", 250000, 2.10, 25.0, 5000, 10, 1000.0, 500.0)
  • Write a class for a different domain : a Vector2D that represents a mathematical vector (x, y).

    • Accept x and y as floats in the initializer
    • Add a read-only property magnitude, the length of the vector, (x**2 + y**2)**0.5
    • Implement __add__(self, other) so that v1 + v2 returns a new Vector2D
    • Implement __mul__(self, scalar) so that v1 * 2.5 scales the vector
    • Override __repr__ so that a vector prints as Vector2D(3.0, 4.0)
  • Print the interactive fleet manager from the course data and read it, the way the closer of Exercise 3 prints the calculator : /lakehouse/default/Files/data/solutions/04.classes/interactive/car.py. It wraps the same classes in a menu that adds, edits, deletes, refuels and drives cars by licence plate, so it runs as a program in a terminal and not in a notebook. Trainer demonstration.

  • Exercise A1 writes the contract of Step 3 down as an abstract base class and as a Protocol, so that a missing method is caught before the loop runs.

Tried it yourself first?

The solution is a spoiler. Work through the hints first : a wrong attempt teaches more than a solution you only read.