5.8. Inheritance Case Study

5.8.1. No Inheritance

>>> class User:
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
...
...     def to_pickle(self):
...         import pickle
...         data = vars(self)
...         return pickle.dumps(data)
...
...     def to_json(self):
...         import json
...         data = vars(self)
...         return json.dumps(data)
>>> mark = User('Mark', 'Watney')
>>>
>>> print(mark.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(mark.to_pickle())
b'\x80\x04\x95,\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94u.'

This class contains methods, which could be also used by other classes, this will lower the amount of code to maintain. So we refactor and Extract superclass.

5.8.2. Single Inheritance

>>> class Serialize:
...     def to_pickle(self):
...         import pickle
...         data = vars(self)
...         return pickle.dumps(data)
...
...     def to_json(self):
...         import json
...         data = vars(self)
...         return json.dumps(data)
>>>
>>>
>>> class User(Serialize):
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>> mark = User('Mark', 'Watney')
>>>
>>> print(mark.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(mark.to_pickle())
b'\x80\x04\x95,\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94u.'

It's better. Now we can reuse Serialize class. However... Is that true, that each class can be serialized to JSON and Pickle at the same time?

5.8.3. Multilevel Inheritance

We can improve code by splitting those capabilities into separate classes. In this case, the Multi level inheritance is a bad pattern here:

>>> class ToJSON:
...     def to_json(self):
...         import json
...         data = vars(self)
...         return json.dumps(data)
>>>
>>> class ToPickle(ToJSON):
...     def to_pickle(self):
...         import pickle
...         data = vars(self)
...         return pickle.dumps(data)
>>>
>>>
>>> class User(ToPickle):
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>> mark = User('Mark', 'Watney')
>>>
>>> print(mark.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(mark.to_pickle())
b'\x80\x04\x95,\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94u.'

It will work as intended for the end-user, but the code structure is disturbed. Not all classes which are serialized to Pickle, are also serialized to JSON. In out case it's a must. This kind of Multi-level inheritance could be found in languages which does not support Multiple inheritance.

5.8.4. Composition

Java is such language which does not have Multiple inheritance. In that case, developers are not using inheritance, and they even go to the extreme, by considering inheritance a bad practice. They use composition:

>>> class ToJSON:
...     def to_json(self):
...         import json
...         data = {attrname: attrvalue
...                 for attrname, attrvalue in vars(self).items()
...                 if not attrname.startswith('_')}
...         return json.dumps(data)
>>>
>>> class ToPickle:
...     def to_pickle(self):
...         import pickle
...         data = {attrname: attrvalue
...                 for attrname, attrvalue in vars(self).items()
...                 if not attrname.startswith('_')}
...         return pickle.dumps(data)
>>>
>>>
>>> class User:
...     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)
>>> mark = User('Mark', 'Watney')
>>>
>>> print(mark.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(mark.to_pickle())
b'\x80\x04\x95,\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94u.'

It gives me ability to write something better:

>>> class MyBetterSerializer(ToJSON):
...     def to_json(self):
...         return ...
>>>
>>> mark = User('Mark', 'Watney', json_serializer=MyBetterSerializer)

This work as intended, and nothing changed for the end-user. This maybe a good pattern for Java, but for Python ecosystem is over-engineered (to complex for that particular usecase).

5.8.5. Multiple Inheritance

That was a must, because Java don't have Multiple inheritance and Simple inheritance or Multilevel inheritance was a bad idea. In Python there is Multiple inheritance capability which enables to create a small and specialized classes and mix them together in order to create objects. Those are called Mixin classes and they use multiple inheritance mechanism:

>>> class ToJSON:
...     def to_json(self):
...         import json
...         data = vars(self)
...         return json.dumps(data)
>>>
>>> class ToPickle:
...     def to_pickle(self):
...         import pickle
...         data = vars(self)
...         return pickle.dumps(data)
>>>
>>>
>>> class User(ToJSON, ToPickle):
...     def __init__(self, firstname, lastname):
...         self.firstname = firstname
...         self.lastname = lastname
>>> mark = User('Mark', 'Watney')
>>>
>>> print(mark.to_json())
{"firstname": "Mark", "lastname": "Watney"}
>>>
>>> print(mark.to_pickle())
b'\x80\x04\x95,\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\tfirstname\x94\x8c\x04Mark\x94\x8c\x08lastname\x94\x8c\x06Watney\x94u.'