Closures and Decorators
Add powers to functions.
Ava wants to know how long each of her functions takes, and to log every call. She could add timing code inside each function, but that clutters them, and repeats the same lines everywhere. A decorator lets her add that behaviour from outside, with a single line above the function, leaving the original untouched.

- Timing and logging in every function?
- Repetition and clutter
- A decorator adds behaviour from outside
Closures: functions that remember
A closure is a function that remembers variables from the place where it was created. Here, make multiplier returns a new function, that remembers the factor. Double remembers two, and triple remembers three. Both are separate, and each keeps its own memory. This idea is the key to decorators.
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10))
print(triple(10))20 30
Your first decorator
A decorator is a function that takes a function, and returns a new one, that wraps the original. The at symbol above a definition applies it. Here, shout wraps greet, and upper cases the result. The star args and kwargs make the wrapper accept any arguments, so it works on any function.
def shout(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
return f"welcome, {name}"
print(greet("Ava"))WELCOME, AVA
A timing decorator
Now something useful. The timer decorator measures how long a function takes, and prints it. We use functools wraps, to keep the original name and docstring. Apply it with the at symbol, and every call is timed, without touching the function's body. Logging, caching, and access checks use exactly this pattern.
import time
from functools import wraps
def timer(fn):
@wraps(fn)
def wrapper(*a, **k):
t = time.perf_counter()
result = fn(*a, **k)
ms = (time.perf_counter() - t) * 1000
print(f"{fn.__name__} took {ms >= 0}")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
print(slow_sum(100000))slow_sum took True 4999950000
Decorators with arguments
A decorator can take its own settings, by adding one more level, a function that returns the decorator. Here, repeat takes a number, and runs the wrapped function that many times. It looks complicated at first, but it is always the same shape, three nested functions.
def repeat(times):
def deco(fn):
def wrapper(*a, **k):
for _ in range(times):
fn(*a, **k)
return wrapper
return deco
@repeat(3)
def water(plant):
print("Watering", plant)
water("fern")Watering fern Watering fern Watering fern
A logging decorator
Your turn. Write a decorator called log, that prints, calling, and the function name, before running the function. Apply it to a function, and call it. Ready? Here is one solution. The wrapper prints, then calls the original, and returns its result.
Write a decorator log that prints "calling <name>" before running the function.
Show hint
print inside wrapper, then call fn
Show solution
def log(fn):
def wrapper(*a, **k):
print("calling", fn.__name__)
return fn(*a, **k)
return wrapper
@log
def add(a, b):
return a + b
print(add(2, 3))calling add 5
Summary
Excellent. Closures remember their surroundings, and decorators wrap functions to add timing, logging and more, without changing them. Next, we study the tool behind the with statement, context managers, and then we meet regular expressions.
Key takeaways- Closures remember outer variables
- @decorator wraps a function
- Use functools.wraps