Effective Python 36 - 40

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)