Click here for the first post, which contains the context of this series.
Item #36: Consider itertools for working with iterators and generators.
These are the most important ones:
- chain
- repeat
- cycle
- tree
- zip_longest
- islice
- takewhile
- dropwhile
- filterfalse
- accumulate
- product
- permutations
- combinations
- combinations_with_replacement
Item #37: Compose classes instead of nesting many levels of built-in types.
Consider
class Gradebook:
def __init__(self):
self._grades = {}
def add_student(self, name):
self._grades[name] = defaultdict(list)
def report_grade(self, name, subject, score, weight, notes):
self._grades[name][subject].append((score, weight, notes))
def average_grade(self, name):
return sum(sum(score * weight for score, weight, _ in grades) for grades in self._grades[name].values()) / len(self._grades[name])
Note that it composes dictionaries and long tuples. This is confusing. Instead, do this:
Grade = namedtuple('Grade', 'score weight')
class Subject:
def __init__(self):
self._grades = []
def report_grade(self, score, weight):
self._grades.append(Grade(score, weight))
def average_grade(self):
return sum(grade.score * grade.weight for grade in self._grades)
class Student:
def __init__(self):
self._subjects = defaultdict(Subject)
def get_subject(self, name):
return self._subjects[name]
def average_grade(self):
return sum(subject.average_grade() for subject in self._subjects.values()) / len(self._subjects)
class Gradebook:
def __init__(self):
self._students = defaultdict(Student)
def get_student(self, name):
return self._students[name]
Although it is longer, it is easier to read and extend.
Item #38: Accept functions instead of classes for simple interfaces.
Python has first-class functions, which means that "functions and methods can be passed around and referenced like any other value in the language":
def my_key(x):
return len(x)
my_list = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
my_list.sort(key=my_key)
But if you want to maintain state, then you can do this:
class MyKey:
def __init__(self):
self.count = 0
def __call__(self, x):
self.count += 1
return len(x)
my_key = MyKey()
my_list = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
my_list.sort(key=my_key)
Item #39: Use @classmethod polymorphism to construct objects generically.
The following script is self-explanatory:
class Animal:
def __init__(self, name):
self.name = name
def sound(self):
raise NotImplementedError
@classmethod
def create_animals(cls):
raise NotImplementedError
class Dog(Animal):
def sound(self):
return f'{self.name} says woof'
@classmethod
def create_animals(cls):
return [cls(name) for name in ['Max', 'Buddy', 'Charlie']]
class Cat(Animal):
def sound(self):
return f'{self.name} says meow'
@classmethod
def create_animals(cls):
return [cls(name) for name in ['Simba', 'Milo', 'Tiger']]
for animal in Dog.create_animals() + Cat.create_animals():
print(animal.sound())
Item #40: Initialize parent classes with super.
Although multiple inheritance is not good, consider the following script, which is an example of diamond inheritance:
class MyBaseClass:
def __init__(self, value):
self.value = value
class TimesTwo(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value *= 2
class PlusFive(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value += 5
class MyClass(TimesTwo, PlusFive):
def __init__(self, value):
TimesTwo.__init__(self, value) # !
PlusFive.__init__(self, value)
print(MyClass(3).value)
This does not work as expected; the indicated line is redundant. The correct way to achieve this is to use super:
class MyBaseClass:
def __init__(self, value):
self.value = value
class TimesTwo(MyBaseClass):
def __init__(self, value):
super().__init__(value)
self.value *= 2
class PlusFive(MyBaseClass):
def __init__(self, value):
super().__init__(value)
self.value += 5
class MyClass(TimesTwo, PlusFive):
def __init__(self, value):
super().__init__(value)
print(MyClass(3).value)