10.2. Functional Practice#
This section focuses on applied functional techniques: recursion, context managers, and functools patterns that you can use in larger programs.
10.2.1. Recursion#
Recursion is taught in 4.3 Recursion: a base case stops the calls, and a recursive case calls the function on a smaller problem. Recursion suits functional programming because a recursive function repeats work without mutating a loop counter. This section compares recursion with iteration, and the lru_cache() example later in this notebook caches a recursive function.
10.2.1.1. Recursion vs Iteration#
Advantages of recursion:
Elegant and intuitive for certain problems
Mirrors mathematical definitions
Good for tree/graph traversal
Disadvantages:
Can be slower due to function call overhead
Risk of stack overflow with deep recursion
May use more memory
Python recursion limit: Python has a default recursion limit (usually 1000). You can check it with sys.getrecursionlimit().
### Exercise: Recursive Functions
# 1. Write `count_down(n)` that prints n, n-1, ..., 1, then "Blastoff!".
# Base case: n <= 0 → print "Blastoff!" and return.
# Recursive case: print n, then call count_down(n - 1).
# 2. Write `sum_digits(n)` that returns the sum of the digits of a positive integer.
# Example: sum_digits(1234) → 10; sum_digits(99) → 18.
# Hint: base case is n < 10; use n % 10 and n // 10 for the recursive case.
### Your code starts here.
### Your code ends here.
5
4
3
2
1
Blastoff!
10
18
10.2.2. Context Managers#
Context managers are objects that define what happens at the start and end of a with statement. They ensure proper resource management and cleanup, even if an exception occurs.
They connect back to functional programming here because @contextmanager is itself a decorator: a higher-order function that transforms a generator function into a context manager.
The @contextmanager decorator from contextlib turns a function that contains yield into a context manager. The function runs in three parts:
Setup. When the
withstatement starts, the code beforeyieldruns.Pause. At
yield, the function pauses and hands the yielded value to thewithblock, which binds it to the name afteras. The body of thewithblock then runs.Cleanup. When the
withblock ends, the function resumes afteryield. Put the cleanup code in afinallyclause around theyield, so it runs even if thewithblock raises an exception.
In the timer() example below, the setup records the start time, yield start_time binds that value to start, and the finally clause prints the elapsed time.
A function that pauses at yield is called a generator function. This section uses yield only inside @contextmanager; 11.2 Generators covers yield and generator functions fully.
# Creating context managers using contextlib
from contextlib import contextmanager
@contextmanager
def timer():
import time
start_time = time.perf_counter()
print("Timer started")
try:
yield start_time
finally:
end_time = time.perf_counter()
print(f"Timer finished: {end_time - start_time:.4f} seconds")
# Using the timer context manager
with timer() as start:
import time
time.sleep(0.1) # Simulate some work
print(f"Work started at: {start}")
Timer started
Work started at: 108.466186376
Timer finished: 0.1002 seconds
The finally clause matters most when something goes wrong. In the next example, the second with block raises a ValueError. The context manager prints its cleanup line first, and then the exception keeps traveling outward to the try/except that surrounds the with statement. The context manager does not hide the error; it only guarantees that the cleanup code runs.
from contextlib import contextmanager
@contextmanager
def opened(name):
print(f"Open {name}") # setup: runs when the with block starts
try:
yield name.upper() # pause: this value is bound by `as`
finally:
print(f"Close {name}") # cleanup: runs even if the block raises
# A block that finishes normally
with opened("sales") as label:
print(f"Reading {label}")
# A block that raises: cleanup runs, then the error reaches the outer except
try:
with opened("orders") as label:
print(f"Reading {label}")
raise ValueError("bad record")
except ValueError as error:
print(f"Caught: {error}")
Open sales
Reading SALES
Close sales
Open orders
Reading ORDERS
Close orders
Caught: bad record
10.2.3. The functools Module#
The functools module provides higher-order functions that operate on or
return other functions. Three particularly useful tools are reduce,
partial, and lru_cache.
10.2.3.1. functools.reduce()#
reduce(function, iterable) applies a two-argument function cumulatively
to the items of an iterable, reducing it to a single value.
Think of it as a fold: it combines elements left to right.
from functools import reduce
# Sum all numbers (same as built-in sum, shown for illustration)
total = reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5])
print(total) # 15
# Product of all numbers
product = reduce(lambda acc, x: acc * x, [1, 2, 3, 4, 5])
print(product) # 120
# Flatten a list of lists
nested = [[1, 2], [3, 4], [5, 6]]
flat = reduce(lambda acc, x: acc + x, nested)
print(flat) # [1, 2, 3, 4, 5, 6]
# In practice, prefer a nested comprehension for flattening; it reads more clearly.
print([item for group in nested for item in group])
15
120
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]
10.2.3.2. functools.partial()#
partial(function, *args, **kwargs) returns a new function with some
arguments pre-filled. This is called partial application — useful for
creating specialized versions of general-purpose functions.
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(4)) # 16
print(cube(3)) # 27
# Practical: pre-configure print with a separator
import functools
header = functools.partial(print, sep=' | ')
header('Name', 'Score', 'Grade') # Name | Score | Grade
16
27
Name | Score | Grade
10.2.3.3. functools.lru_cache()#
@lru_cache memoizes a function’s results: the first time a function is
called with given arguments, the result is stored. On subsequent calls with
the same arguments, the cached result is returned immediately — no
recomputation needed.
LRU stands for Least Recently Used: when the cache is full, the least recently accessed entry is evicted.
The example below decorates the recursive Fibonacci function from 4.3 Recursion. Without the cache, fib(n) solves the same subproblems again and again; with the cache, each fib(k) is computed once and then looked up.
from functools import lru_cache
import time
@lru_cache(maxsize=None) # unlimited cache
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# Without cache, fib(40) would take seconds (2^40 calls)
start = time.perf_counter()
print(fib(50)) # 12586269025
elapsed = time.perf_counter() - start
print(f'Computed in {elapsed:.6f}s') # microseconds with cache
# Inspect cache statistics
print(fib.cache_info()) # CacheInfo(hits=..., misses=51, maxsize=None, currsize=51)
12586269025
Computed in 0.000116s
CacheInfo(hits=48, misses=51, maxsize=None, currsize=51)
### Exercise: functools
# 1. Use reduce() to find the maximum value in [3, 1, 4, 1, 5, 9, 2, 6].
# (Hint: lambda acc, x: acc if acc > x else x)
# 2. Use partial() to create `add10` — a function that adds 10 to any number.
# Test: add10(5) → 15, add10(-3) → 7.
# 3. Decorate `count_ways(n)` below with @lru_cache to speed it up.
# count_ways(n) returns how many ways to climb n stairs (1 or 2 at a time).
from functools import reduce, partial, lru_cache
def count_ways(n):
if n <= 1: return 1
return count_ways(n - 1) + count_ways(n - 2)
### Your code starts here.
### Your code ends here.
9
15
7
1346269
10.2.4. Summary#
Concept |
Key idea |
|---|---|
Lambda |
Anonymous single-expression function: |
|
Apply a function to every element: |
|
Keep elements where function returns |
|
Sort by custom criterion: |
List comprehension |
|
Dict comprehension |
|
Set comprehension |
|
Decorator |
Wraps a function to add behavior without modifying it; use |
|
Preserves the wrapped function’s name and docstring |
Recursion |
Function that calls itself; requires a base case + recursive case |
Context manager |
|
|
Setup before |