6.1. OOP Inheritance Patterns¶
no inheritance
single inheritance
multilevel inheritance
multiple inheritance (mixin classes)
- single inheritance¶
One class inherits from one other class. Has one parent.
- multilevel inheritance¶
One class inherits from other class, and yet another class inherits from it. This creates hierarchical structure.
- multiple inheritance¶
- mixin classes¶
One class derives from several other classes at once.
6.1.1. No Inheritance¶
>>> class Vehicle:
... pass
>>>
>>>
>>> class Car:
... pass
6.1.2. Single Inheritance¶
>>> class Vehicle:
... pass
>>>
>>>
>>> class Car(Vehicle):
... pass
6.1.3. Multilevel Inheritance¶
>>> class Vehicle:
... pass
>>>
>>>
>>> class VehicleWithWindows(Vehicle):
... pass
>>>
>>>
>>> class Car(VehicleWithWindows):
... pass
6.1.4. Multiple Inheritance¶
HasEngine
andHasWindows
are Mixin ClassesSuch classes are usually called:
EngineMixin
,WindowsMixin
>>> class Vehicle:
... pass
>>>
>>> class HasEngine:
... pass
>>>
>>> class HasWindows:
... pass
>>>
>>>
>>> class Car(Vehicle, HasEngine, HasWindows):
... pass
...
6.1.5. Aggregation¶
>>> class Vehicle:
... pass
...
>>> class Engine:
... pass
...
>>> class Windows:
... pass
...
>>>
>>> class Car(Vehicle):
... parts = [Engine, Windows]
6.1.6. Composition¶
>>> class Vehicle:
... pass
>>>
>>> class Engine:
... pass
...
>>> class Windows:
... pass
...
>>>
>>> class Car(Vehicle):
... engine = Engine
... windows = Windows
6.1.7. Why Composition?¶
>>> class Mother:
... pass
>>>
>>> class Father:
... pass
>>>
>>>
>>> class Child:
... mother: Mother
... father: Father
...
... def __init__(self, mother=Mother, father=Father):
... self.mother = mother()
... self.father = father()
>>> class StepFather:
... pass
>>>
>>> me = Child(father=StepFather)
6.1.8. Use Case - 0x01¶
Following example is simple and easy to understand, but not totally
accurate. Inheritance means, that a class is a specialized form of
its base. This results in a subclass being an instance of a superclass.
Which is weird when we think, that a Child
might be its Parent
in the same time.
No Inheritance:
>>> class Parent:
... pass
>>>
>>>
>>> class Child:
... pass
Single Inheritance:
>>> class Parent:
... pass
>>>
>>>
>>> class Child(Parent):
... pass
Multilevel Inheritance:
>>> class Grandparent:
... pass
>>>
>>> class Parent(Grandparent):
... pass
>>>
>>>
>>> class Child(Parent):
... pass
Multiple Inheritance:
>>> class Mother:
... pass
>>>
>>> class Father:
... pass
>>>
>>>
>>> class Child(Mother, Father):
... pass
Aggregation:
>>> class Mother:
... pass
>>>
>>> class Father:
... pass
>>>
>>> class Child:
... parents = [Father, Mother]
Composition:
>>> class Mother:
... pass
>>>
>>> class Father:
... pass
>>>
>>> class Child:
... mother = Mother
... father = Father
6.1.9. Use Case - 0x02¶
>>> class Mother:
... pass
>>>
>>> class Father:
... pass
>>>
>>>
>>> class Child:
... mother: Mother
... father: Father
...
... def __init__(self, mother=Mother, father=Father):
... self.mother = mother()
... self.father = father()
6.1.10. Use Case - 0x03¶
>>> class Vehicle:
... engine: Engine
... windows: Windows | None
>>>
>>> class Engine:
... def engine_start(self): ...
... def engine_stop(self): ...
...
>>> class Windows:
... def window_open(self): ...
... def window_close(self): ...
...
>>>
>>> class Car(Vehicle):
... engine: Engine
... windows: Windows
...
... def __init__(self, windows=Windows, engine=Engine):
... self.windows = windows()
... self.engine = engine()
...
... def engine_start(self):
... if self.engine:
... return self.engine.engine_start()
...
... def engine_stop(self):
... if self.engine:
... return self.engine.engine_stop()
...
... def window_open(self):
... if self.windows:
... return self.windows.windows_open()
...
... def window_close(self):
... if self.windows:
... return self.windows.windows_close()
6.1.11. Use Case - 0x04¶
>>> class Encoder:
... def encode(self, data):
... ...
>>>
>>> class Decoder:
... def decode(self, data):
... ...
>>>
>>>
>>> class JSONSerializer:
... encoder: Encoder
... decoder: Decoder
...
... def __init__(self,
... encoder: Encoder = Encoder,
... decoder: Decoder = Decoder,
... ) -> None:
... self.encoder = encoder()
... self.decoder = decoder()
...
... def encode(self, data):
... return self.encoder.encode(data)
...
... def decode(self, data):
... return self.decoder.decode(data)
Now, if you want to serialize your data, just create an instance
and call method .encode()
on it.
>>> DATA = {'firstname': 'Mark', 'lastname': 'Watney'}
>>> json = JSONSerializer()
>>> result = json.encode(DATA)
If you want to use your better version of encoder (for example which
can encode datetime
object. You can create a class which inherits
from default Encoder
and overwrite .encode()
method.
>>> class MyBetterEncoder(Encoder):
... def encode(self, data):
... ...
>>>
>>> DATA = {'firstname': 'Mark', 'lastname': 'Watney'}
>>> json = JSONSerializer(encoder=MyBetterEncoder)
>>> result = json.encode(DATA)
6.1.12. Use Case - 0x05¶
>>> from datetime import date
>>> import json
JSON dumps works well, we we don't have any exotic types:
>>> DATA = {'firstname': 'Mark', 'lastname': 'Watney'}
>>> json.dumps(DATA)
'{"firstname": "Mark", "lastname": "Watney"}'
When we introduce date
object, JSON cannot serialize it:
>>> DATA = {'firstname': 'Mark', 'lastname': 'Watney', 'birthdate': date(1969, 7, 21)}
>>> json.dumps(DATA)
Traceback (most recent call last):
TypeError: Object of type date is not JSON serializable
We can introduce MyEncoder
and inject it into json.dumps()
function:
>>> class MyEncoder(json.JSONEncoder):
... def default(self, x):
... if isinstance(x, date):
... return x.isoformat()
...
>>>
>>> DATA = {'firstname': 'Mark', 'lastname': 'Watney', 'birthdate': date(1969, 7, 21)}
>>> json.dumps(DATA, cls=MyEncoder)
'{"firstname": "Mark", "lastname": "Watney", "birthdate": "1969-07-21"}'
6.1.13. Use Case - 0x06¶
Base classes:
>>> class Engine:
... pass
>>>
>>> class Windows:
... pass
>>>
>>> class Radio:
... pass
Standard Classes:
>>> class StandardEngine(Engine):
... pass
>>>
>>> class StandardWindows(Windows):
... pass
>>>
>>> class StandardRadio(Radio):
... pass
Define Vehicle
using composition:
>>> class Vehicle:
... engine: Engine
... windows: Windows | None
... radio: Radio | None
...
... def __init__(self, engine=StandardEngine, windows=StandardWindows, radio=StandardRadio):
... self.engine = engine()
... self.windows = windows()
... self.radio = radio()
Usage:
>>> class PoorEngine(Engine):
... ...
>>>
>>> class ElectricEngine(Engine):
... ...
>>>
>>> class SuperRadio(Radio):
... ...
>>>
>>> mercedes = Vehicle()
>>> maluch = Vehicle(engine=PoorEngine)
>>> tesla = Vehicle(engine=ElectricEngine, radio=SuperRadio)
If not specified, car will use Standard part (such as StandardEngine
or StandardRadio
). Thanks to composition we can override defaults.
6.1.14. Use Case - 0x07¶
>>> class Account:
... def __init__(self, username):
... self.username = username
>>>
>>> class User(Account):
... pass
>>>
>>> class Admin(Account):
... pass
>>>
>>>
>>> class Group:
... members: list[Account] = [
... User('mwatney'),
... Admin('mlewis'),
... User('rmartinez'),
... ]
6.1.15. Use Case - 0x08¶
>>> import json
>>> import pickle
>>>
>>>
>>> class User:
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
...
... def to_json(self):
... data = vars(self)
... return json.dumps(data)
...
... def to_pickle(self):
... return pickle.dumps(self)
...
... def to_csv(self):
... values = tuple(vars(self).values())
... return ','.join(values) + '\n'
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.to_json()
'{"firstname": "Mark", "lastname": "Watney"}'
>>>
>>> mark.to_pickle()
b'\x80\x04\x95D\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94)\x81\x94}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94ub.'
>>>
>>> mark.to_csv()
'Mark,Watney\n'
6.1.16. Use Case - 0x09¶
>>> import json
>>> import pickle
>>>
>>>
>>> class Serializable:
... def to_json(self):
... data = vars(self)
... return json.dumps(data)
...
... def to_pickle(self):
... return pickle.dumps(self)
...
... def to_csv(self):
... values = tuple(vars(self).values())
... return ','.join(values) + '\n'
>>> class User(Serializable):
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
>>>
>>>
>>> class Admin(Serializable):
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
>>>
>>>
>>> class Group(Serializable):
... def __init__(self, members):
... self.members = members
...
... def to_csv(self):
... raise NotImplementedError
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.to_json()
'{"firstname": "Mark", "lastname": "Watney"}'
>>>
>>> mark.to_pickle()
b'\x80\x04\x95D\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94)\x81\x94}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94ub.'
>>>
>>> mark.to_csv()
'Mark,Watney\n'
6.1.17. Use Case - 0x0A¶
>>> import json
>>> import pickle
>>>
>>>
>>> class JSONSerializable:
... def to_json(self):
... data = vars(self)
... return json.dumps(data)
>>>
>>> class PickleSerializable:
... def to_pickle(self):
... return pickle.dumps(self)
>>>
>>> class CSVSerializable:
... def to_csv(self):
... values = tuple(vars(self).values())
... return ','.join(values) + '\n'
>>> class User(JSONSerializable, PickleSerializable, CSVSerializable):
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
>>>
>>>
>>> class Admin(JSONSerializable, PickleSerializable, CSVSerializable):
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
>>>
>>>
>>> class Group(JSONSerializable, PickleSerializable):
... def __init__(self, members):
... self.members = members
>>> mark = User('Mark', 'Watney')
>>>
>>> mark.to_json()
'{"firstname": "Mark", "lastname": "Watney"}'
>>>
>>> mark.to_pickle()
b'\x80\x04\x95D\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04User\x94\x93\x94)\x81\x94}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94ub.'
>>>
>>> mark.to_csv()
'Mark,Watney\n'
6.1.18. Use Case - 0x0B¶
>>> import json
>>> import pickle
>>> from typing import Type
>>>
>>>
>>> class JSONSerializer:
... @staticmethod
... def to_json(instance):
... fields = {k:v for k,v in vars(instance).items() if not k.startswith('_')}
... return json.dumps(fields)
>>>
>>> class PickleSerializer:
... @staticmethod
... def to_pickle(instance):
... fields = {k:v for k,v in vars(instance).items() if not k.startswith('_')}
... return pickle.dumps(fields)
>>>
>>> class CSVSerializer:
... @staticmethod
... def to_csv(instance):
... fields = {k:v for k,v in vars(instance).items() if not k.startswith('_')}
... values = tuple(fields.values())
... return ','.join(values) + '\n'
>>>
>>>
>>> class Serializable:
... _json_serializer: Type[JSONSerializer] | None = None
... _pickle_serializer: Type[PickleSerializer] | None = None
... _csv_serializer: Type[CSVSerializer] | None = None
...
... def to_json(self, cls=_json_serializer):
... if cls is not None:
... return cls.to_json(self)
...
... def to_pickle(self, cls=_pickle_serializer):
... if cls is not None:
... return cls.to_pickle(self)
...
... def to_csv(self, cls=_csv_serializer):
... if cls is not None:
... return cls.to_csv(self)
Groups are not CSV serializable:
>>> class Group(Serializable):
... _json_serializer = JSONSerializer
... _pickle_serializer = PickleSerializer
... _csv_serializer = None
>>>
>>> class User(Serializable):
... _json_serializer = JSONSerializer
... _pickle_serializer = PickleSerializer
... _csv_serializer = CSVSerializer
...
... def __init__(self, firstname, lastname):
... self.firstname = firstname
... self.lastname = lastname
Dump to JSON using default serializer:
>>> mark = User('Mark', 'Watney')
>>> mark.to_json()
Dump to JSON using custom serializer:
>>> class MyJSONSerializer(JSONSerializer):
... @staticmethod
... def to_json(instance):
... ...
>>>
>>> mark = User('Mark', 'Watney')
>>> mark.to_json(cls=MyJSONSerializer)
6.1.19. Further Reading¶
6.1.20. Assignments¶
"""
* Assignment: OOP InheritancePatterns SingleInheritance
* Type: class assignment
* Complexity: easy
* Lines of code: 2 lines
* Time: 2 min
English:
1. Create class `Account`
2. Create class `User` which inherits from `Account`
3. Create class `Admin` which inherits from `Account`
4. Run doctests - all must succeed
Polish:
1. Stwórz klasę `Account`
2. Stwórz klasę `User`, która dziedziczy po `Account`
3. Stwórz klasę `Admin`, która dziedziczy po `Account`
4. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass
>>> assert isclass(Account)
>>> assert isclass(User)
>>> assert isclass(Admin)
>>> assert issubclass(User, Account)
>>> assert issubclass(Admin, Account)
>>> assert not issubclass(User, Admin)
>>> assert not issubclass(Admin, User)
"""
# Create class `Account`
# type: type[Account]
class Account:
pass
# Create class `User` which inherits from `Account`
# type: type[Account]
class User:
pass
# Create class `Admin` which inherits from `Account`
# type: type[Account]
class Admin:
pass
"""
* Assignment: OOP InheritancePatterns NoInheritance
* Complexity: easy
* Lines of code: 8 lines
* Time: 3 min
English:
1. Create classes `MyAccount`, `Account`, `User`, `Admin`
2. Do not use inheritance
3. Assignment demonstrates syntax, so do not add any attributes and methods
4. Run doctests - all must succeed
Polish:
1. Stwórz klasy `MyAccount`, `Account`, `User`, `Admin`
2. Nie używaj dziedziczenia
3. Zadanie demonstruje składnię, nie dodawaj żadnych atrybutów i metod
4. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass
>>> assert isclass(Account)
>>> assert isclass(User)
>>> assert isclass(Admin)
>>> assert isclass(MyAccount)
"""
# Create classes `MyAccount`, `Account`, `User`, `Admin`
# Do not use inheritance, do not define attributes
"""
* Assignment: OOP InheritancePatterns Multilevel
* Complexity: easy
* Lines of code: 3 lines
* Time: 2 min
English:
1. Create class `MyAccount` from classes `Account`, `User`, `Admin`
2. Use multilevel inheritance
3. Assignment demonstrates syntax, so do not add any attributes and methods
4. Run doctests - all must succeed
Polish:
1. Stwórz klasę `MyAccount` z klas `Account`, `User`, `Admin`
2. Użyj wielopoziomowego dziedziczenia
3. Zadanie demonstruje składnię, nie dodawaj żadnych atrybutów i metod
4. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass
>>> assert isclass(Account)
>>> assert isclass(User)
>>> assert isclass(Admin)
>>> assert isclass(MyAccount)
>>> assert issubclass(MyAccount, Account)
>>> assert issubclass(MyAccount, User)
>>> assert issubclass(MyAccount, Admin)
>>> assert issubclass(Account, object)
>>> assert issubclass(User, Account)
>>> assert issubclass(Admin, User)
>>> assert issubclass(MyAccount, Admin)
>>> assert len(Account.__subclasses__()) == 1
>>> assert len(User.__subclasses__()) == 1
>>> assert len(Admin.__subclasses__()) == 1
>>> assert len(MyAccount.__subclasses__()) == 0
"""
# Create classes `MyAccount`, `Account`, `User`, `Admin`
# Use multilevel inheritance, do not define attributes
class Account:
pass
class User:
pass
class Admin:
pass
class MyAccount:
pass
"""
* Assignment: OOP InheritancePatterns Multiple
* Complexity: easy
* Lines of code: 1 lines
* Time: 2 min
English:
1. Create class `MyAccount` from classes `Account`, `User`, `Admin`
2. Use mixins classes
3. Assignment demonstrates syntax, so do not add any attributes and methods
4. Run doctests - all must succeed
Polish:
1. Stwórz klasę `MyAccount` z klas `Account`, `User`, `Admin`
2. Użyj klas domieszkowych (mixin)
3. Zadanie demonstruje składnię, nie dodawaj żadnych atrybutów i metod
4. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass
>>> assert isclass(Account)
>>> assert isclass(User)
>>> assert isclass(Admin)
>>> assert isclass(MyAccount)
>>> assert issubclass(MyAccount, Account)
>>> assert issubclass(MyAccount, User)
>>> assert issubclass(MyAccount, Admin)
>>> assert len(Account.__subclasses__()) == 1
>>> assert len(User.__subclasses__()) == 1
>>> assert len(Admin.__subclasses__()) == 1
>>> assert len(MyAccount.__subclasses__()) == 0
"""
# Create classes `MyAccount`, `Account`, `User`, `Admin`
# Use mixins classes, do not define attributes
class Account:
pass
class User:
pass
class Admin:
pass
class MyAccount:
pass
"""
* Assignment: OOP InheritancePatterns Composition
* Complexity: easy
* Lines of code: 2 lines
* Time: 3 min
English:
1. Define class `MyAccount` with attribute `type: Account`
2. Use composition
3. Allow for setting type `User` or `Admin` in `__init__()` method
4. Assignment demonstrates syntax
5. Run doctests - all must succeed
Polish:
1. Zdefiniuj klasę `MyAccount` z atrybutem `type: Account`
2. Użyj kompozycji
3. Pozwól na ustawianie typu `User` or `Admin` w metodzie `__init__()`
4. Zadanie demonstruje składnię
5. Uruchom doctesty - wszystkie muszą się powieść
Tests:
>>> import sys; sys.tracebacklimit = 0
>>> from inspect import isclass
>>> assert isclass(User)
>>> assert isclass(Admin)
>>> assert isclass(Account)
>>> assert isclass(MyAccount)
>>> result = MyAccount()
>>> assert hasattr(result, 'type')
>>> result = MyAccount()
>>> assert hasattr(result, 'type')
>>> assert not isclass(result.type)
>>> assert isinstance(result.type, Account)
>>> assert not isinstance(result.type, User)
>>> assert not isinstance(result.type, Admin)
>>> result = MyAccount(type=Account)
>>> assert hasattr(result, 'type')
>>> assert not isclass(result.type)
>>> assert isinstance(result.type, Account)
>>> assert not isinstance(result.type, User)
>>> assert not isinstance(result.type, Admin)
>>> result = MyAccount(type=User)
>>> assert hasattr(result, 'type')
>>> assert not isclass(result.type)
>>> assert isinstance(result.type, Account)
>>> assert isinstance(result.type, User)
>>> assert not isinstance(result.type, Admin)
>>> result = MyAccount(type=Admin)
>>> assert hasattr(result, 'type')
>>> assert not isclass(result.type)
>>> assert isinstance(result.type, Account)
>>> assert not isinstance(result.type, User)
>>> assert isinstance(result.type, Admin)
"""
class Account:
pass
class User(Account):
pass
class Admin(Account):
pass
# Use composition
# Define class `MyAccount` with attribute `type: Account`
# Allow for setting type `User` or `Admin` in `__init__()` method
class MyAccount:
pass