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