Effective Python 26 - 30

Click here for the first post, which contains the context of this series.

Item #26: Define function decorators with functools.wraps.

Function decorators add functionality before and after the execution of the functions that they decorate. For example,

def fib(n):
    if n < 3:
        return 1
    return fib(n - 1) + fib(n - 2)

fib(500)

will probably never terminate. An idea is to cache:

cache = {}

def fib(n):
    if n in cache:
        return cache[n]
    if n < 3:
        return 1
    cache[n] = fib(n - 1) + fib(n - 2)
    return cache[n]

fib(500)

But what if you want to do this to multiple functions? An idea is to use decorators:

from functools import wraps

def my_cache(func):
    cache = {}
    @wraps(func)
    def wrapper(n):
        if n in cache:
            return cache[n]
        cache[n] = func(n)
        return cache[n]
    return wrapper

@my_cache # decorator
def fib(n):
    if n < 3:
        return 1
    return fib(n - 1) + fib(n - 2)

@my_cache
def new_fib(n):
    if n < 4:
        return 1
    return new_fib(n - 1) + new_fib(n - 2) + new_fib(n - 3)

fib(500)
new_fib(500)

You can add more than one decorator to a function; note the order in which they are applied.

Item #27: Use comprehensions instead of map and filter.

Let

a = [1, 2, 3, 4, 5, 6, 7, 8, 9]

Suppose that you are doing this:

b = []
for c in a:
    if c % 2:
        b.append(c ** 2)

A better way is to use a comprehension:

b = [c ** 2 for c in a if c % 2]

Avoid using map and filter:

b = list(map(lambda c: c ** 2, filter(lambda c: c % 2, a)))

Comprehensions can also be used with dict and set.

Item #28: Avoid more than two control sub-expressions in comprehensions.

You can stack comprehensions:

a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
b = [[x, y] for x in a if x % 2 for y in a if not y % 2]

Avoid stacking more than two, and note the order in which the for loops are executed.

Item #29: Avoid repeated work in comprehensions by using assignment expressions.

Consider

[expensive_function(x) for x in X if meets_condition(expensive_function(x))]

This is repeated work because expensive_function could be called twice for each x. Instead, use an assignment expression:

[result for x in X if meets_condition(result := expensive_function(x))]

Note that result leaks out of the scope of the comprehension.

Item #30: Consider generators instead of returning lists.

Consider a function that returns a list of anagrams of a word:

def get_anagrams(word, anagram='', is_free=None):
    if is_free is None:
        is_free = [True for _ in word]
    if not any(is_free):
        return [anagram]
    else:
        anagrams = []
        for i, _ in enumerate(is_free):
            if is_free[i]:
                is_free[i] = False
                anagrams += get_anagrams(word, anagram + word[i], is_free)
                is_free[i] = True
        return anagrams

Suppose that you want to print the first 10 anagrams of the word "incomprehensible":

for i, anagram in enumerate(get_anagrams('incomprehensible')):
    print(anagram)
    if i == 9:
        break

This will probably never terminate: the problem is that get_anagrams('incomprehensible') attempts to create a list of the 2.092279e+13 anagrams of this word. A generator does not have this problem:

def get_anagrams(word, anagram='', is_free=None):
    if is_free is None:
        is_free = [True for _ in word]
    if not any(is_free):
        yield anagram
    else:
        for i, _ in enumerate(is_free):
            if is_free[i]:
                is_free[i] = False
                yield from get_anagrams(word, anagram + word[i], is_free)
                is_free[i] = True