5.5. Inheritance vs. Composition

5.5.1. Rationale

  • Composition over Inheritance

5.5.2. Code Duplication

class Car(Vehicle):
    def engine_start(self):
        pass

    def engine_stop(self):
        pass


class Truck(Vehicle):
    def engine_start(self):
        pass

    def engine_stop(self):
        pass

5.5.3. Inheritance

class Vehicle:
    def engine_start(self):
        pass

    def engine_stop(self):
        pass


class Car(Vehicle):
    pass


class Truck(Vehicle):
    pass

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

5.5.5. Composition

  • 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

5.5.6. Case Study

Listing 5.96. Multi level inheritance is a bad pattern here
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'\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.'
Listing 5.97. 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.'

5.5.7. Assignments

5.5.7.1. OOP Composition Mars

  • Assignment name: OOP Composition Mars

  • Last update: 2020-10-01

  • Complexity level: easy

  • Lines of code to write: 8 lines

  • Estimated time of completion: 3 min

  • Solution: solution/oop_composition_mars.py

English
  1. Create class Habitat

  2. Create class Rocket

  3. Create class Astronaut

  4. Compose class MarsMission from Habitat, Rocket, Astronaut

  5. Assignment demonstrates syntax, so do not add any attributes and methods

  6. Compare result with "Output" section (see below)

Polish
  1. Stwórz klasę Habitat

  2. Stwórz klasę Rocket

  3. Stwórz klasę Astronaut

  4. Skomponuj klasę MarsMission z Habitat, Rocket, Astronaut

  5. Zadanie demonstruje składnię, nie dodawaj żadnych atrybutów i metod

  6. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Output
>>> 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)

5.5.7.2. OOP Composition Movable

  • Assignment name: OOP Composition Movable

  • Last update: 2020-10-01

  • Complexity level: medium

  • Lines of code to write: 20 lines

  • Estimated time of completion: 13 min

  • Solution: solution/oop_composition_movable.py

English
  1. Define class Point

  2. Class Point has attributes x: int = 0 and y: int = 0

  3. When x or y has negative value raise en exception ValueError('Coordinate cannot be negative')

  4. Define class Movable

  5. In Movable define method get_position(self) -> Point

  6. In Movable define method set_position(self, x: int, y: int) -> None

  7. In Movable define method change_position(self, left: int = 0, right: int = 0, up: int = 0, down: int = 0) -> None

  8. Assume left-top screen corner as a initial coordinates position:

    1. going right add to x

    2. going left subtract from x

    3. going up subtract from y

    4. going down add to y

  9. Compare result with "Output" section (see below)

Polish
  1. Zdefiniuj klasę Point

  2. Klasa Point ma atrybuty x: int = 0 oraz y: int = 0

  3. Gdy x lub y mają wartość ujemną podnieś wyjątek ValueError('Coordinate cannot be negative')

  4. Zdefiniuj klasę Movable

  5. W Movable zdefiniuj metodę get_position(self) -> Point

  6. W Movable zdefiniuj metodę set_position(self, x: int, y: int) -> None

  7. W Movable zdefiniuj metodę change_position(self, left: int = 0, right: int = 0, up: int = 0, down: int = 0) -> None

  8. Przyjmij górny lewy róg ekranu za punkt początkowy:

    • idąc w prawo dodajesz x

    • idąc w lewo odejmujesz x

    • idąc w górę odejmujesz y

    • idąc w dół dodajesz y

  9. Porównaj wyniki z sekcją "Output" (patrz poniżej)

Output
>>> from inspect import isclass, ismethod
>>> assert isclass(Point)
>>> assert isclass(Movable)
>>> assert hasattr(Point, 'x')
>>> assert hasattr(Point, 'y')
>>> assert hasattr(Movable, 'get_position')
>>> assert hasattr(Movable, 'set_position')
>>> assert hasattr(Movable, 'change_position')
>>> assert ismethod(Movable().get_position)
>>> assert ismethod(Movable().set_position)
>>> assert ismethod(Movable().change_position)

>>> class Astronaut(Movable):
...     pass

>>> astro = Astronaut()

>>> astro.set_position(x=1, y=2)
>>> astro.get_position()
Point(x=1, y=2)

>>> astro.set_position(x=1, y=1)
>>> astro.change_position(right=1)
>>> astro.get_position()
Point(x=2, y=1)

>>> astro.set_position(x=1, y=1)
>>> astro.change_position(left=1)
>>> astro.get_position()
Point(x=0, y=1)

>>> astro.set_position(x=1, y=1)
>>> astro.change_position(down=1)
>>> astro.get_position()
Point(x=1, y=2)

>>> astro.set_position(x=1, y=1)
>>> astro.change_position(up=1)
>>> astro.get_position()
Point(x=1, y=0)

>>> astro.set_position(x=1, y=1)
>>> astro.change_position(left=2)
Traceback (most recent call last):
    ...
ValueError: Coordinate cannot be negative

>>> astro.set_position(x=1, y=1)
>>> astro.change_position(up=2)
Traceback (most recent call last):
    ...
ValueError: Coordinate cannot be negative