AI Hints
Home›Python Tutorial›Generators, Decorators and Context Managers
Python · Lesson 23 of 49

Closures and Decorators

Add powers to functions.

⏱ 2 min read▶ Video at 1:12:20🎓 Free
Watch this lesson in the Python courseWatch on YouTube ↗Full Python course

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.

Closures and Decorators
From the video: Closures and Decorators
  • 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.

closure.pypython
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))
Output
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.

deco1.pypython
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"))
Output
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.

timer.pypython
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))
Output
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.

deco_args.pypython
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")
Output
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.

✍️ Exercise: A logging decorator

Write a decorator log that prints "calling <name>" before running the function.

Show hint

print inside wrapper, then call fn

Show solution
log_deco.pypython
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))
Output
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
Next up: Context Managers and Regex — safe resources, and pattern matching