S7.0 What This Teaches
- First-class functions and higher-order functions
- Closures: nested functions capturing outer variables
- Decorators: transforming functions with
@syntax functools.partialfor partial applicationfunctools.wrapsfor preserving metadata- Lambda expressions
S7.1 First-Class Functions
def double(x: int) -> int:
return x * 2
def apply(func, values: list) -> list:
return [func(v) for v in values]
print(apply(double, [1, 2, 3, 4])) # [2, 4, 6, 8]
print(apply(str, [1, 2, 3])) # ['1', '2', '3']
print(apply(abs, [-1, -2, 3])) # [1, 2, 3]
# Functions as dict values
ops = {"+": lambda a, b: a + b, "-": lambda a, b: a - b}
print(ops["+"](3, 4)) # 7
S7.2 Closures
def make_adder(delta: int):
def adder(x: int) -> int:
return x + delta # captures delta from outer scope
return adder
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(3)) # 8
print(add10(3)) # 13
# Counter using a closure with mutable state
def make_counter(start: int = 0):
count = [start] # list to allow mutation in inner scope
def increment():
count[0] += 1
return count[0]
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
S7.3 Decorators
@decorator syntax is shorthand for
func = decorator(func):
import time
from functools import wraps
def timer(func):
@wraps(func) # preserves func.__name__, __doc__, etc.
def wrapper(*args, **kwargs):
start = time.monotonic()
result = func(*args, **kwargs)
elapsed = time.monotonic() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_sum(n: int) -> int:
return sum(range(n))
print(slow_sum(1_000_000)) # prints timing and result
S7.4 Stacking and Parameterized Decorators
from functools import wraps
def repeat(n: int):
"""Decorator factory that runs the function n times."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
def log(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log
@repeat(3) # applied bottom-up: repeat first, then log
def greet(name: str):
print(f"Hello, {name}!")
greet("Alice")
S7.5 functools Utilities
from functools import partial, reduce
# partial - freeze some arguments
def power(base: float, exp: float) -> float:
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
print(square(4)) # 16.0
print(cube(3)) # 27.0
# reduce - fold a sequence to a single value
product = reduce(lambda acc, x: acc * x, [1, 2, 3, 4, 5])
print(product) # 120
# lru_cache - memoize with Least Recently Used eviction
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n: int) -> int:
if n < 2: return n
return fib(n - 1) + fib(n - 2)
print(fib(40)) # fast due to caching
S7.6 Example - All Together
# Closures - validation decorator and pipeline composition.
from functools import wraps, reduce
def validate(*predicates):
"""Decorator that checks all predicates before calling the function."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for pred in predicates:
if not pred(*args, **kwargs):
raise ValueError(f"validation failed: {pred.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
def positive(x): return x > 0
def less_than_100(x): return x < 100
@validate(positive, less_than_100)
def process(x: float) -> float:
return x * 2
print(process(42)) # 84.0
try:
print(process(-1)) # raises ValueError
except ValueError as e:
print(e)
S7.7 Exercise
Exercise
- Write a
memoizedecorator that caches results in a dict. Apply it to a recursive Fibonacci function and compare performance with and without the decorator. - Write a
retry(n)parameterized decorator that retries a function up to n times on exception. Test it with a function that fails randomly. - Use
functools.partialto create specialized versions of a genericformat_number(value, width, decimal_places)function.
S7.8 Common Mistakes
Forgetting @wraps on a decorator
def my_decorator(func):
def wrapper(*args, **kwargs): # missing @wraps(func)
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(): pass
print(greet.__name__) # "wrapper" - lost the original name!
@functools.wraps(func) inside a decorator so that
__name__, __doc__, and other metadata are preserved.
Late binding in closures
adders = [lambda x: x + i for i in range(5)]
print(adders[0](0)) # 4, not 0! All lambdas capture the same i=4
# Fix: capture current value with a default argument
adders = [lambda x, i=i: x + i for i in range(5)]
print(adders[0](0)) # 0
S7.9 Key Terms
| Term | Meaning |
|---|---|
| first-class function | Function that can be assigned, passed, and returned like any value |
| closure | Inner function that captures and remembers variables from its enclosing scope |
| decorator | Function that wraps another function, typically added with @syntax |
| @wraps | Copies __name__, __doc__, etc. from the wrapped function to the wrapper |
| partial | Creates a new function with some arguments pre-filled |
| reduce | Folds a sequence to a single value by applying a two-argument function |
| lru_cache | Memoization decorator with configurable cache size; caches recent results |
| late binding | Closures capture variable references, not values - value resolved at call time |