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¶
"""
* 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. Run doctests - all must succeed
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. Uruchom doctesty - wszystkie muszą się powieść
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
"""
* 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. Run doctests - all must succeed
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. Uruchom doctesty - wszystkie muszą się powieść
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