How to disentangle child and parent __init__ methods in Python?

Viewed 75

I am trying to find the right way to use inheritance when the order for the attributes being set for the parent class will break in the child class. Here's an example:

class Car:
    def __init__(self, efficiency, tank_size) -> None:
        self.efficiency = efficiency
        self.tank_size = tank_size
        self.range = self.calc_range()

    def calc_range(self):
        return self.efficiency * self.tank_size


class HybridCar(Car):
    def __init__(self, efficiency, tank_size, battery_capacity) -> None:
        super().__init__(efficiency, tank_size)
        self.battery_range = self.calc_battery_range()
        self.battery_capacity = battery_capacity

    def calc_range(self):
        return self.efficiency * self.tank_size + self.battery_range

    def calc_battery_range(self):
        return self.battery_capacity * self.efficiency

I would like HybridCar to call Car's __init__ method but I can't get it to work. I can't call self.calc_range() in HybridCar because I don't have self.battery_range defined. And I can't define self.battery_range because that relies on self.efficiency, which I haven't defined yet. The only way I can see to make this work is for HybridCar not to call Car's __init__ method but instead have all the same code. I want to avoid this because there's a lot of code in the __init__ method (the real class does much more than this) and seems like bad practice to have lots of duplicated code (And pylint warns me about not calling super().__init__). What's the right way to solve this?

2 Answers

Would something like the following help you?

class Car:
    def __init__(self, efficiency, tank_size) -> None:
        self.efficiency = efficiency
        self.tank_size = tank_size
        self.range = self.calc_range_Car()

    def calc_range_Car(self):
        return self.efficiency * self.tank_size


class HybridCar(Car):
    def __init__(self, battery_capacity, car) -> None:
        super().__init__(car.efficiency, car.tank_size)
        self.battery_capacity = battery_capacity
        self.battery_range = self.calc_battery_range()

    def calc_range_Hybrid(self):
        return self.efficiency * self.tank_size + self.battery_range

    def calc_battery_range(self):
        return self.battery_capacity * self.efficiency

parent = Car(2, 5)
child = HybridCar(10, parent)
print(child.calc_range_Car())
print(parent.range)

The only thing I do differently is that I a Car object to the HybridCar constructor as well as renaming the method calc_range to calc_range_Car and calc_range_Hybrid. I understand that this might not be ideal because overriding superclass method in the subclass is really useful. Output

10
10

I think you want to use a @property definition in your class. This means that your assignment of self.range = self.calc_range() function won't actually be executed during the __init__(). But instead self.calc_range() will be run each time you are trying to get the range property.

You also mentioned that there is a lot of potential duplicate code, so in the example below I've included the use of the abc (Abstract Base Class) concept. Which is a pretty handy concept when you have inherited classes that share methods with the same name, but have different implementations.

import abc


class CarABC(abc.ABC):
    def __init__(self, efficiency, tank_size) -> None:
        self.efficiency = efficiency
        self.tank_size = tank_size

    @abc.abstractmethod
    def calc_range(self):
        pass

    @property
    def range(self):
        return self.calc_range()


class Car(CarABC):
    def calc_range(self):
        return self.efficiency * self.tank_size


class HybridCar(CarABC):
    def __init__(self, efficiency, tank_size, battery_capacity) -> None:
        super().__init__(efficiency, tank_size)
        self.battery_capacity = battery_capacity

    @property
    def battery_range(self):
        return self.calc_battery_range()

    def calc_range(self):
        return self.efficiency * self.tank_size + self.battery_range

    def calc_battery_range(self):
        return self.battery_capacity * self.efficiency

if __name__ == "__main__":
    c = Car(0.8, 100)
    hc = HybridCar(0.8, 100, 120)
    print(c.range)
Related