4.5. Inheritance vs. Composition¶
4.5.1. Rationale¶
Composition over Inheritance
4.5.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
4.5.3. Inheritance¶
class Vehicle:
def engine_start(self):
pass
def engine_stop(self):
pass
class Car(Vehicle):
pass
class Truck(Vehicle):
pass
4.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
4.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
4.5.6. Case Study¶
Multi level inheritance is a bad pattern here:
class A:
pass
class B(A):
pass
class C(B):
pass
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.'
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.'
4.5.7. Assignments¶
"""
* Assignment: OOP Composition Syntax
* Filename: oop_composition_syntax.py
* 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
"""
* Assignment: OOP Composition Decompose
* Filename: oop_composition_decompose.py
* 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`
3. 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`
3. 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 __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
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[int, int]:
return self._position_x, self._position_y