Functional Python

Use the built-ins that replace a loop, pass functions as values, and get a cache for one line.

18of 24
≈ 12Min read
5Parts

Functions are values

A function is an ordinary object: you can store it, pass it and return it. A dict of functions is Python’s switch statement, and it is usually clearer than the if/elif chain it replaces.

py
def shout(text):
    return text.upper()

f = shout               # no parentheses: the function itself
f("hi")                 # 'HI'

handlers = {"click": on_click, "key": on_key}     # a dict of functions
handlers["click"](event)

sorted(words, key=len)                 # a function passed as an argument
sorted(words, key=str.casefold)        # an unbound method works too

The folds that replace a loop

any and all short-circuit, so passing a generator to them means the work stops at the first answer. all([]) is True and any([]) is False — the mathematically correct answers, and both surprise people once.

py
sum(prices)                       # and sum(prices, start=10)
min(people, key=lambda p: p.age)
max(counts.values())
any(w.isupper() for w in words)   # stops at the first True
all(x > 0 for x in values)        # stops at the first False
sorted(rows, key=lambda r: (-r["score"], r["name"]))

sum(1 for x in items if x.ok)     # count matching, without building a list

lambda, and when not to

A lambda is a single expression with no name and no statements. It earns its place as an argument to sorted, min, max or groupby; assigning one to a variable just gives you a worse def, with no name in the traceback.

And map(lambda x: f(x), xs) is always [f(x) for x in xs] spelled worse — a comprehension is the Python way to map and filter.

py
sorted(people, key=lambda p: p.age)       # good: one expression, used once

f = lambda x: x * 2                      # bad: just write def, you get a name
sorted(words, key=lambda w: w.lower())   # fine, but str.lower is shorter
sorted(nums, key=lambda n: abs(n))       # use key=abs

functools

@cache memoises on the arguments, which must be hashable. It is one line that turns an exponential recursion into a linear one — and a memory leak if you put it on something with unbounded inputs, which is why lru_cache(maxsize=...) exists.

py
from functools import reduce, partial, lru_cache, cache

reduce(operator.mul, nums, 1)          # fold — rare in Python, and that is fine
open_utf8 = partial(open, encoding="utf-8")   # a function with an argument fixed

@cache                                  # unbounded; @lru_cache(maxsize=1000) to bound it
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

fib(200)            # instant — without the cache this never finishes
fib.cache_info()    # CacheInfo(hits=..., misses=..., ...)

operator, and method chaining

itemgetter and attrgetter are the two worth remembering: they say exactly what a lambda would, in less space, and they take several keys at once.

py
from operator import itemgetter, attrgetter, methodcaller

sorted(rows, key=itemgetter("score"))          # faster and clearer than a lambda
sorted(people, key=attrgetter("last", "first"))
max(counts.items(), key=itemgetter(1))         # the most common

Practice