5.10. OOP Inheritance Problems

5.10.1. About

Please excuse me, for code style in this chapter. Instead writing:

>>> class Car:
...     def engine_start(self):
...         ...
...
...     def engine_stop(self):
...         ...

I will write:

>>> class Car:
...     def engine_start(self): ...
...     def engine_stop(self): ...

This way the code is more dense and idea is much clearer to present. There won't be any method implementations in examples.

5.10.2. Problem

  • Code duplication

>>> class Car:
...     def engine_start(self): ...
...     def engine_stop(self): ...
>>>
>>> class Truck:
...     def engine_start(self): ...
...     def engine_stop(self): ...

5.10.3. Simple Inheritance

>>> class Vehicle:
...     def engine_start(self): ...
...     def engine_stop(self): ...
>>>
>>>
>>> class Car(Vehicle):
...     pass
>>>
>>> class Truck(Vehicle):
...     pass
../../_images/uml-relations-inheritance-simple.png

5.10.4. Inheritance Problem

  • Motorcycle is a vehicle, but doesn't have windows

>>> class Vehicle:
...     def engine_start(self): ...
...     def engine_stop(self): ...
>>>
>>>
>>> class Car(Vehicle):
...     def window_open(self): ...
...     def window_close(self): ...
>>>
>>>
>>> class Truck(Vehicle):
...     def window_open(self): ...
...     def window_close(self): ...
>>>
>>>
>>> class Motorcycle(Vehicle):
...     pass

5.10.5. Not Implemented Error

>>> class Vehicle:
...     def engine_start(self): ...
...     def engine_stop(self): ...
...     def window_open(self): ...
...     def window_close(self): ...
>>>
>>>
>>> class Car(Vehicle):
...     pass
>>>
>>> class Truck(Vehicle):
...     pass
>>>
>>> class Motorcycle(Vehicle):
...     def windows_open(self): raise NotImplementedError
...     def windows_close(self): raise NotImplementedError

5.10.6. Multilevel Inheritance

>>> class Vehicle:
...     def engine_start(self): ...
...     def engine_stop(self): ...
>>>
>>> class VehicleWithWindows(Vehicle):
...     def window_open(self): ...
...     def window_close(self): ...
>>>
>>>
>>> class Car(VehicleWithWindows):
...     pass
>>>
>>> class Truck(VehicleWithWindows):
...     pass
>>>
>>> class Motorcycle(Vehicle):
...     pass
../../_images/uml-relations-inheritance-multilevel.png

5.10.7. Problem

  • Code duplication or another multilevel inheritance

>>> class Vehicle:
...     def engine_start(self): ...
...     def engine_stop(self): ...
>>>
>>> class VehicleWithWindows(Vehicle):
...     def window_open(self): ...
...     def window_close(self): ...
>>>
>>>
>>> class Car(VehicleWithWindows):
...     pass
>>>
>>> class Truck(VehicleWithWindows):
...     def cbradio_on(self): ...
...     def cbradio_off(self): ...
>>>
>>> class Motorcycle(Vehicle):
...     def cbradio_on(self): ...
...     def cbradio_off(self): ...

5.10.8. Solution With Multilevel Inheritance

  • Tuck is both VehicleWithWindows and VehicleWithCBRadio

  • Causes more problem

  • This is why in other languages composition is preferred over inheritance

>>> class Vehicle:
...     def engine_start(self): ...
...     def engine_stop(self): ...
>>>
>>> class VehicleWithWindows(Vehicle):
...     def window_open(self): ...
...     def window_close(self): ...
>>>
>>> class VehicleWithCBRadio(Vehicle):
...     def cbradio_on(self): ...
...     def cbradio_off(self): ...
>>>
>>>
>>> class Car(VehicleWithWindows):
...     pass
>>>
>>> class Truck():  # both: VehicleWithWindows and VehicleWithCBRadio
...     pass
>>>
>>> class Motorcycle(VehicleWithCBRadio):
...     pass

5.10.9. Solution With Mixin Classes

  • This is the Pythonic solution

>>> class Vehicle:
...     def engine_start(self): ...
...     def engine_stop(self): ...
>>>
>>> class HasWindows:
...     def window_open(self): ...
...     def window_close(self): ...
>>>
>>> class HasCBRadio:
...     def cbradio_on(self): ...
...     def cbradio_off(self): ...
>>>
>>>
>>>
>>> class Car(Vehicle, HasWindows):
...     pass
>>>
>>> class Truck(Vehicle, HasWindows, HasCBRadio):
...     pass
>>>
>>> class Motorcycle(Vehicle, HasCBRadio):
...     pass

5.10.10. Cleanup

>>> class Vehicle:
...     pass
>>>
>>> class HasEngine:
...     def engine_start(self): ...
...     def engine_stop(self): ...
>>>
>>> class HasWindows:
...     def window_open(self): ...
...     def window_close(self): ...
>>>
>>> class HasCBRadio:
...     def cbradio_on(self): ...
...     def cbradio_off(self): ...
>>>
>>>
>>> class Car(Vehicle, HasEngine, HasWindows):
...     pass
>>>
>>> class Truck(Vehicle, HasEngine, HasWindows, HasCBRadio):
...     pass
>>>
>>> class Motorcycle(Vehicle, HasEngine, HasCBRadio):
...     pass
../../_images/uml-relations-mixin.png

5.10.11. Case Study

Problem:

>>> class Astronaut:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
...
...     def to_pickle(self):
...         import pickle
...         data = vars(self)
...         return pickle.dumps(data)
...
...     def to_json(self):
...         import json
...         data = vars(self)
...         return json.dumps(data)

This class contains methods, which could be also used by other classes, this will lower the amount of code to maintain. So we refactor and Extract superclass.

>>> class Serialize:
...     def to_pickle(self):
...         import pickle
...         data = vars(self)
...         return pickle.dumps(data)
...
...     def to_json(self):
...         import json
...         data = vars(self)
...         return json.dumps(data)
>>>
>>>
>>> class Astronaut(Serialize):
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname

It's better. Now we can reuse Serialize class. However... Is that true, that each class can be serialized to JSON and Pickle at the same time?

We can improve code by splitting those capabilities into separate classes. In this case, the Multi level inheritance is a bad pattern here:

>>> class ToJSON:
...     def to_json(self):
...         import json
...         data = vars(self)
...         return json.dumps(data)
>>>
>>> class ToPickle(ToJSON):
...     def to_pickle(self):
...         import pickle
...         data = vars(self)
...         return pickle.dumps(data)
>>>
>>>
>>> class Astronaut(ToPickle):
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> print(astro.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(astro.to_pickle())  
b'\x80\x04\x95I\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\tAstronaut' \
b'\x94\x93\x94)\x81\x94}\x94(\x8c\tfirstname\x94\x8c\x04Mark' \
b'\x94\x8c\x08lastname\x94\x8c\x06Watney\x94ub.'

It will work as intended for the end-user, but the code structure is disturbed. Not all classes which are serialized to Pickle, are also serialized to JSON. In out case it's a must. This kind of Multi-level inheritance could be found in languages which does not support Multiple inheritance. Java is such language. In that case, developers are not using inheritance, and they even go to the extreme, by considering inheritance a bad practice. They use composition:

>>> class ToJSON:
...     def to_json(self):
...         import json
...         data = {attrname: attrvalue
...                 for attrname, attrvalue in vars(self).items()
...                 if not attrname.startswith('_')}
...         return json.dumps(data)
>>>
>>> class ToPickle:
...     def to_pickle(self):
...         import pickle
...         data = {attrname: attrvalue
...                 for attrname, attrvalue in vars(self).items()
...                 if not attrname.startswith('_')}
...         return pickle.dumps(data)
>>>
>>>
>>> class Astronaut:
...     firstname: str
...     lastname: str
...     __json_serializer: ToJSON
...     __pickle_serializer: ToPickle
...
...     def __init__(self, firstname, lastname, json_serializer=ToJSON, pickle_serializer=ToPickle):
...         self.firstname = firstname
...         self.lastname = lastname
...         self.__json_serializer = json_serializer
...         self.__pickle_serializer = pickle_serializer
...
...     def to_json(self):
...         return self.__json_serializer.to_json(self)
...
...     def to_pickle(self):
...         return self.__pickle_serializer.to_pickle(self)
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> print(astro.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(astro.to_pickle())  
b'\x80\x04\x95\xa3\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\tAstronaut\x94\x93\x94)\x81\x94}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94\x8c\x1b_Astronaut__json_serializer\x94h\x00\x8c\x06ToJSON\x94\x93\x94\x8c\x1d_Astronaut__pickle_serializer\x94h\x00\x8c\x08ToPickle\x94\x93\x94ub.'
>>>
>>>
>>> # It give me ability to write something better
>>> class MyBetterSerializer(ToJSON):
...     def to_json(self):
...         return ...
>>>
>>> astro = Astronaut('Mark', 'Watney', json_serializer=MyBetterSerializer)

This work as intended, and nothing changed for the end-user. This maybe a good pattern for Java, but for Python ecosystem is over-engineered (to complex for that particular usecase).

That was a must, because Java don't have Multiple inheritance and Simple inheritance or Multilevel inheritance was a bad idea. In Python there is Multiple inheritance capability which enables to create a small and specialized classes and mix them together in order to create objects. Those are called Mixin classes and they use multiple inheritance mechanism:

>>> class ToJSON:
...     def to_json(self):
...         import json
...         data = vars(self)
...         return json.dumps(data)
>>>
>>> class ToPickle:
...     def to_pickle(self):
...         import pickle
...         data = vars(self)
...         return pickle.dumps(data)
>>>
>>>
>>> class Astronaut(ToJSON, ToPickle):
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>>
>>>
>>> astro = Astronaut('Mark', 'Watney')
>>>
>>> print(astro.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(astro.to_pickle())  
b'\x80\x04\x95I\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\tAstronaut' \
b'\x94\x93\x94)\x81\x94}\x94(\x8c\tfirstname\x94\x8c\x04Mark' \
b'\x94\x8c\x08lastname\x94\x8c\x06Watney\x94ub.'

5.10.12. Assignments

Code 5.22. Solution
"""
* Assignment: OOP InheritanceProblems Decompose
* Complexity: easy
* Lines of code: 30 lines
* Time: 8 min

English:
    1. Refactor class `Hero` to use multiple inheritance
    2. Name mixin classes: `HasHealth` and `HasPosition`
    3. Note, that order of inheritance is important
        a. Try to inherit from `HasPosition`, `HasHealth`
        b. Then `HasHealth`, `HasPosition`
        c. What changed and why?
    4. Run doctests - all must succeed

Polish:
    1. Zrefaktoruj klasę `Hero` aby użyć wielodziedziczenia
    2. Nazwij klasy domieszkowe: `HasHealth` i `HasPosition`
    3. Zwróć uwagę, że kolejność dziedziczenia ma znaczenie
        a. Spróbuj dziedziczyć po `HasPosition`, `HasHealth`
        b. A później `HasHealth`, `HasPosition`
        c. Co się zmieniło i dlaczego?
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from random import seed; seed(0)
    >>> from inspect import isclass

    >>> assert isclass(Hero)
    >>> assert isclass(HasHealth)
    >>> assert isclass(HasPosition)
    >>> assert issubclass(Hero, HasHealth)
    >>> assert issubclass(Hero, HasPosition)
    >>> assert hasattr(HasHealth, 'HEALTH_MIN')
    >>> assert hasattr(HasHealth, 'HEALTH_MAX')
    >>> assert hasattr(HasHealth, 'health')
    >>> assert hasattr(HasHealth, 'is_alive')
    >>> assert hasattr(HasHealth, 'is_dead')
    >>> assert hasattr(HasPosition, 'position_x')
    >>> assert hasattr(HasPosition, 'position_y')
    >>> assert hasattr(HasPosition, 'position_set')
    >>> assert hasattr(HasPosition, 'position_change')
    >>> assert hasattr(HasPosition, 'position_get')
    >>> assert hasattr(Hero, 'HEALTH_MIN')
    >>> assert hasattr(Hero, 'HEALTH_MAX')
    >>> assert hasattr(Hero, 'health')
    >>> assert hasattr(Hero, 'position_x')
    >>> assert hasattr(Hero, 'position_y')
    >>> assert hasattr(Hero, 'is_alive')
    >>> assert hasattr(Hero, 'is_dead')
    >>> assert hasattr(Hero, 'position_set')
    >>> assert hasattr(Hero, 'position_change')
    >>> assert hasattr(Hero, 'position_get')
    >>> watney = Hero()
    >>> watney.is_alive()
    True
    >>> watney.position_set(x=1, y=2)
    >>> watney.position_change(left=1, up=2)
    >>> watney.position_get()
    (0, 0)
    >>> watney.position_change(right=1, down=2)
    >>> watney.position_get()
    (1, 2)
"""

from dataclasses import dataclass
from random import randint


@dataclass
class Hero:
    HEALTH_MIN: int = 10
    HEALTH_MAX: int = 20
    health: int = 0
    position_x: int = 0
    position_y: int = 0

    def position_set(self, x: int, y: int) -> None:
        self.position_x = x
        self.position_y = y

    def position_change(self, right=0, left=0, down=0, up=0):
        x = self.position_x + right - left
        y = self.position_y + down - up
        self.position_set(x, y)

    def position_get(self) -> tuple:
        return self.position_x, self.position_y

    def __post_init__(self) -> None:
        self.health = randint(self.HEALTH_MIN, self.HEALTH_MAX)

    def is_alive(self) -> bool:
        return self.health > 0

    def is_dead(self) -> bool:
        return self.health <= 0