Click here for the first post, which contains the context of this series.
Item #40: Consider composing functionality with mix-in classes.
Although you should avoid multiple inheritance, you can use a mix-in class, which is a class that only defines a small set of methods.
Consider the desire to represent an object as a dictionary:
class ToDictMixin:
def to_dict(self):
return self._traverse_dict(self.__dict__)
def _traverse_dict(self, instance_dict):
output = {}
for key, value in instance_dict.items():
output[key] = self._traverse(key, value)
return output
def _traverse(self, key, value):
if isinstance(value, ToDictMixin):
return value.to_dict()
if isinstance(value, dict):
return self._traverse_dict(value)
if isinstance(value, list):
return [self._traverse(key, i) for i in value]
if hasattr(value, '__dict__'):
return self._traverse_dict(value.__dict__)
return value
and consider the desire to represent an object as a JSON string:
class ToJsonMixin:
@classmethod
def from_json(cls, data):
kwargs = json.loads(data)
return cls(**kwargs)
def to_json(self):
return json.dumps(self.to_dict())
Then
class BinaryTree(ToDictMixin, ToJsonMixin):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
has a lot of useful functionality.
Item #41: Prefer public attributes over private ones.
Consider
class MyClass:
def __init__(self, value):
self.__value = value
my_class = MyClass(5)
Then my_class.__value will raise an AttributeError, but it still can be accessed by my_class._MyClass__value, so Python does not really enforce privacy.
To avoid making your code cumbersome and brittle, avoid doing this. Instead, do this
class MyClass:
def __init__(self, value):
self._value = value
and document that self._value is protected.
Item #43: Inherit from collections.abc for custom container types.
I can extend list.
class MyList(list):
def __init__(self, elements):
super().__init__(elements)
def frequencies(self):
count = {}
for item in self:
count[item] = count.get(item, 0) + 1
return count
But what if I want to do this for something that is not inherently a list?
class BinaryNode:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
def _traverse(self):
if self.left:
yield from self.left._traverse()
yield self
if self.right:
yield from self.right._traverse()
def __getitem__(self, index):
for i, item in enumerate(self._traverse()):
if i == index:
return item
raise IndexError('binary tree index out of range')
However, using len on a BinaryNode object will raise an exception. The question becomes: what is the least amount of methods that I need to implement to behave like a list? Enter collections.abc:
from collections.abc import Sequence
class BinaryNode(Sequence):
# ...
This raises an exception with the names of the methods that need to be implemented.
Item #44: Use plain attributes instead of setter and getter methods.
This is done in other languages:
class IWasACSharpDev:
def __init__(self, value):
self._value = value
def get_value(self):
return self._value
def set_value(self, value):
self._value = value
But this is not Pythonic. We use @property if we need to do this:
class IAmAPythonDev:
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
@value.setter
def value(self, value):
self._value = value
Make sure to keep them short and quick.
Item #45: Consider @property instead of refactoring attributes.
Consider
class Person:
def __init__(self, age):
self.age = age
suppose that the codebase sets and gets the ages of countless instances of this class, and suppose that a new law requires that a person's new age be three more than twice their old age. Only a small change needs to be made:
class Person:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age * 2 + 3
@age.setter
def age(self, age):
self._age = age