2

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?

timthedev07
  • 454
  • 1
  • 6
  • 17
jss367
  • 4,759
  • 14
  • 54
  • 76
  • Maybe set once efficiency in the beginning of hybrid car init. It's a bit ugly but already better than copying all of car init. – NoDataDumpNoContribution Mar 23 '21 at 06:30
  • This kind of thing is why calling overridable methods in a constructor is a bad idea. – user2357112 Mar 23 '21 at 06:38
  • I thought that was OK in Python... is it not? Isn't that what they're saying here or am I misinterpreting it? https://stackoverflow.com/a/6859112/2514130 – jss367 Mar 23 '21 at 06:47
  • That answerer is interpreting "this does not inherently raise an exception" as "this is safe". Calling an overridden method in a superclass constructor is a recipe for horrible circular dependency issues (like what you're seeing now), even if the mere attempt to do so does not raise an exception on its own. – user2357112 Mar 23 '21 at 06:52

2 Answers2

0

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
  • This works because `calc_range` is simple enough that you can essentially remove it and do find `self.range` directly. In my real case, there's a lot more going on so it wouldn't work as well. I was struggling to find a way to ask the question with a minimal example that still contained everything important, but seems like I didn't do that perfectly. – jss367 Mar 23 '21 at 18:25
  • Is it possible to post your whole script and expected output? I will try modify my answer according to your needs – Pavlos Rousoglou Mar 23 '21 at 18:43
  • It adds way more complexity than it's worth. I think the fundamental problem I'm dealing with is here, I just need a more scalable solution. – jss367 Mar 23 '21 at 19:53
  • The answer is updated. Does this work for you or do you need your superclass function to override the subclass function? – Pavlos Rousoglou Mar 23 '21 at 20:43
0

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)
Adehad
  • 519
  • 6
  • 16