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