Introduction
-
As an oop language, Python supports a full range of features, such as inheritance, polymorphism, and encapsulation
-
Python’s classes and inheritance make it easy to express a program’s intended behaviors with objects.
-
They allow you to improve and expand functionality over time.
-
They provide flexibility in an environment of changing requirements.
-
Item 37. Compose Classes Instead of Nesting Many Levels of Build-in Types
-
Avoid making dictionaries with values that are dictionaries, long tuples, or complex nestings of other build-in types.
-
Use
namedtuple
for lightweight, immutalbe data containers before you need the flexibility of a full class. -
Move your bookkeeping code to using multiple classes when your internal state dictionaries get complicated
# Before : nested build-in types in dictionary
class BySubjectGradeBook:
def __init__(self):
self._grades = {}
def add_student(self, name):
self._grades[name] = defaultdict(list)
def report_grade(self, name, subject, grade):
by_subject = self._grades[name]
grade_list = by_subject[subject]
grade_list.append(grade)
def average_grade(self, name):
by_subject = self._grades[name]
total, count = 0, 0
for grades in by_subject.values():
total += sum(grades)
coutn += len(grades)
return total/count
# After : Use multiple classes
class Subject:
def __init__(self):
self._grades = []
def report_grades(self, score, weight):
self._grades.append(Grade(score, weight))
def average_grade(self):
return total / total_weight # omitted details
class Student:
def __init__(self):
self._subjects = defaultdict(Subject)
def get_subject(self, name):
return self._subjects[name]
def average_grade(self):
return total / count # omitted details
class Gradebook:
def __init__(self):
self._students = defaultdict(Student)
def get_student(self, name):
return self._students[name]
Item 39. Use @classmethod
Polymorphism to Construct Object Generically
-
Python only supports a single constructor per class: the
__init__
method. -
Use
@classmethod
to define alternative constructors for your classes. -
Use class method polymorphism to provide generic ways to build and connect many concrete subclasses.
Item 40. Initialize Parent Classes with super
-
Python’s standard method resolution order solves the problems of superclass initialization order and diamond inheritance
-
Use the
super
built-in function with zero arguments to initialize parent classes
# Diamond inheritance
class MyBaseClass:
def __init__(self, value):
self.value = value
class TimesSeven(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value *= 7
class PlusNine(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value += 9
class ThisWay(TimesSeven, PlusNine):
def __init__(self, value):
TimesSeven.__init__(self, value)
PlusNine.__init__(self, value) # This line resets the value to 5
# >> Should be (5 * 7) + 9 = 44 but is 14
# The call to the second parent class’s constructor, PlusNine.__init__, causes self.value to be reset back to 5 when MyBaseClass
# To solve these problems, Python has the super built-in function and standard method resolution order (MRO).
# super ensures that common superclasses in diamond hierarchies are run only once
class TimesSevenCorrect(MyBaseClass):
def __init__(self, value):
super().__init__(value)
self.value *= 7
class PlusNineCorrect(MyBaseClass):
def __init__(self, value):
super().__init__(value)
self.value += 9
# Now, the top part of the diamond, MyBaseClass.__init__, is run only a single time. The other parent classes are run in the order specified in the class statement
class GoodWay(TimesSevenCorrect, PlusNineCorrect):
def __init__(self, value):
super().__init__(value)
Item41. Consider Composing Functionality with Mix-in Classes
-
Avoid using multiple inheritance with instance attributes and
__init__
if mix-in classes can achieve the same outcome. -
Use pluggable behaviors at the instance level to provide per-class customization when mix-in classes may require it.
-
Mix-ins can include instance methods or class methods, depending on your needs.
-
Compose mix-ins to create complex functionality from simple behaviors.
# Define an example mix-in that accomplishes
class ToDictMixin:
def to_dict(self):
return self._traverse_dict(self.__dict__)
def _traverse_dict(self, instance_dict):
output = {}
for k,v 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()
elif isinstance(value, dict):
return self._traverse_dict(value)
elif isinstance(value, list):
return [self.traverse(key, i) for i in value]
else:
return value
# Define an example class that uses the mix-in to make a dictionary representation of a binary tree
class BinaryTree(ToDictMixin):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
# Translating a large number of related Python objects into a dict becomes easy:
tree = BinaryTree(10,
left = BinaryTree(7, right=BinaryTree(9)),
right = BinaryTree(13, left=BinaryTree(11)))
Item42. Prefer Public Attributes Over Private Ones
-
Private attributes aren’t regorously enforced by the Python complier
-
Plan from the beginning to allow subclasses to do more with your internal APIs and attributes instead of choosing to lock them out.
-
Use documentation of protected fields to guide subclasses instead of trying to force access control with private attributes
-
Only consider using private attributes to avoid naming conflicts with subclasses that are out of your control
# In Python, there are only two types of visibility for a class’s attributes: public and private
Class MyObject:
def __init__(self):
self.public_field = 5
self.__private_field = 10
def get_private_field(self):
return self.__privage_field
# class methdos also have access to private attributes
@classmethod
def get_private_field_of_instance(cls, instance):
return instance.__private_field
Class MyChildObject(MyObject):
def get_private_field(self):
return self.__private_field
Item43. Inherit from collections.abc
for Custom Container Types
-
Inherit dicectly from Python’s container types (like list or dict) for simple use cases
-
Beware of the large number of methods required to implement custom container types correctly
-
Have yout custom container types inherit from the interfaces defined in
collections.abc
to ensure that your classes match required interfaces and behaviors
# When you access a sequence item by index:
bar = [1,2,3]
bar[0]
# above line will be interpreted as :
bar.__getitem__(0)
# So, to make your own class act like a sequence, you can provide a custom implementation of __getitem__
class MyClass():
def __init__(self, values):
self.values = values
def __getitem__(self, index):
for i, item in enumerate(values):
if i == index:
return item
raies IndexError(f'Inted {index} is out of range')
# But it's not enough to provide all of the sequence semantics you'd expect from a list instance
test = MyClass(values=[1,2,3])
len(test) # >> MyClass has no len()
# The built-in collections.abc module defines a set of abstract base classes that provide all of the typical methods for each container type
from collections.abc import Sequence
class BadType(Sequence):
pass
foo = BadType()
>>>
Traceback ...
TypeError: Can't instantiate abstract class BadType with ̄abstract methods __getitem__, __len__
# When you do implement all the methods required by an abstract base class from collections.abc, it provides all of the additional methods, like index, count, ... for free.