6.7. Inheritance vs. Composition

6.7.1. Rationale

  • Composition over Inheritance

6.7.2. Code Duplication

class Car:
    def engine_start(self):
        pass

    def engine_stop(self):
        pass


class Truck:
    def engine_start(self):
        pass

    def engine_stop(self):
        pass

6.7.3. Inheritance

class Vehicle:
    def engine_start(self):
        pass

    def engine_stop(self):
        pass


class Car(Vehicle):
    pass


class Truck(Vehicle):
    pass

6.7.4. Inheritance Problem

class Vehicle:
    def engine_start(self):
        pass

    def engine_stop(self):
        pass

    def window_open(self):
        pass

    def window_close(self):
        pass


class Car(Vehicle):
    pass


class Truck(Vehicle):
    pass


class Motorbike(Vehicle):
    """Motorbike is a vehicle, but doesn't have windows."""

    def window_open(self):
        raise NotImplementedError

    def window_close(self):
        raise NotImplementedError

6.7.5. Multilevel Inheritance

class A:
    pass

class B(A):
    pass

class C(B):
    pass

6.7.6. Composition

class Vehicle:
    pass

class HasEngine:
    def engine_start(self):
        pass

    def engine_stop(self):
        pass

class HasWindows:
    def window_open(self):
        pass

    def window_close(self):
        pass


class Car(Vehicle):
    engine: HasEngine
    window: HasWindows

class Truck(Vehicle):
    engine: HasEngine
    window: HasWindows

class Motorbike(Vehicle):
    engine: HasEngine
    window: None

6.7.7. Mixin Classes

class Vehicle:
    pass

class HasEngine:
    def engine_start(self):
        pass

    def engine_stop(self):
        pass

class HasWindows:
    def window_open(self):
        pass

    def window_close(self):
        pass


class Car(Vehicle, HasEngine, HasWindows):
    pass

class Truck(Vehicle, HasEngine, HasWindows):
    pass

class Motorbike(Vehicle, HasEngine):
    pass

6.7.8. Case Study

Multi level inheritance is a bad pattern here .. code-block:: python

class ToJSON:
def to_json(self):

import json return json.dumps(self.__dict__)

class ToPickle(ToJSON):
def to_pickle(self):

import pickle return pickle.dumps(self)

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'x80x04x95Ix00x00x00x00x00x00x00x8cx08__main__x94x8ctAstronaut' # b'x94x93x94)x81x94}x94(x8ctfirstnamex94x8cx04Mark' # b'x94x8cx08lastnamex94x8cx06Watneyx94ub.'

Composition:

class ToJSON:
    def to_json(self):
        import json
        data = {k: v for k, v in vars(self).items() if not k.startswith('_')}
        return json.dumps(data)

class ToPickle:
    def to_pickle(self):
        import pickle
        return pickle.dumps(self)


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)

Mixin classes - multiple inheritance:

class ToJSON:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class ToPickle:
    def to_pickle(self):
        import pickle
        return pickle.dumps(self)


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.'

6.7.9. Assignments

Code 6.17. Solution
"""
* Assignment: OOP Composition Syntax
* Complexity: easy
* Lines of code: 2 lines
* Time: 3 min

English:
    1. Use data from "Given" section (see below)
    2. Compose class `MarsMission` from `Habitat`, `Rocket`, `Astronaut`
    3. Assignment demonstrates syntax, so do not add any attributes and methods
    4. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Skomponuj klasę `MarsMission` z `Habitat`, `Rocket`, `Astronaut`
    3. Zadanie demonstruje składnię, nie dodawaj żadnych atrybutów i metod
    4. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> from inspect import isclass
    >>> assert isclass(Habitat)
    >>> assert isclass(Astronaut)
    >>> assert isclass(Rocket)
    >>> assert isclass(MarsMission)
    >>> assert issubclass(MarsMission, Habitat)
    >>> assert issubclass(MarsMission, Astronaut)
    >>> assert issubclass(MarsMission, Rocket)
"""


# Given
class Habitat:
    pass


class Astronaut:
    pass


class Rocket:
    pass


Code 6.18. Solution
"""
* Assignment: OOP Composition Decompose
* Complexity: easy
* Lines of code: 30 lines
* Time: 13 min

English:
    1. Use data from "Given" section (see below)
    2. Refactor class `Hero` to use composition
    3. Name mixin classes: `HasHealth` and `HasPosition`
    4. Note, that order of inheritance is important
        a. Try to inherit from `HasPosition`, `HasHealth`
        b. Then `HasHealth`, `HasPosition`
        c. What changes?
    5. Compare result with "Tests" section (see below)

Polish:
    1. Użyj danych z sekcji "Given" (patrz poniżej)
    2. Zrefaktoruj klasę `Hero` aby użyć kompozycji
    3. Nazwij klasy domieszkowe: `HasHealth` i `HasPosition`
    4. 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?
    5. Porównaj wyniki z sekcją "Tests" (patrz poniżej)

Tests:
    >>> 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_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, '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)
"""


# Given
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