Decorators and Closures
Decorators are one of Python's most elegant features. They let you "add extras" to functions without modifying the original code — adding logging, timing, permission checks... Many frameworks and libraries use decorators extensively. Understanding them is an important milestone in Python mastery.
1. Closures
Closures are the foundation of decorators. Simply put: an inner function remembers variables from the outer function, even after the outer function has finished executing.
def make_multiplier(n):
"""Create a multiplication function"""
def multiplier(x):
return x * n # Inner function remembers the outer variable n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20
Even though double and triple come from the same factory function make_multiplier, they each remember different n values (2 and 3). This is a closure — a function + the external variables it remembers.
Tip: Closure use cases: 1) Factory functions (creating differently configured functions as above). 2) Decorators (standard implementation). 3) Saving context in callback functions.
2. Decorator Basics
A decorator is a function that takes a function as an argument and returns a new function:
def my_decorator(func):
def wrapper():
print("=== Before function execution ===")
func()
print("=== After function execution ===")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
=== Before function execution ===
Hello!
=== After function execution ===
@my_decorator is equivalent to say_hello = my_decorator(say_hello). The syntax sugar makes the code cleaner.
Example: Logging Decorator (Difficulty: Star-Star)
def log_call(func):
"""Log function calls"""
def wrapper(*args, **kwargs):
print(f"[LOG] Calling {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} returned {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(add(3, 5))
print(greet("Zhang San"))
Output:
[LOG] Calling add((3, 5), {})
[LOG] add returned 8
8
[LOG] Calling greet(('Zhang San',), {'greeting': 'Hello'})
[LOG] greet returned Hello, Zhang San!
3. functools.wraps: Preserving Original Function Info
Decorators "replace" the original function, causing the function name and docstring to be lost. @wraps fixes this:
from functools import wraps
def my_decorator(func):
@wraps(func) # Preserve original function info
def wrapper(*args, **kwargs):
"""Decorator inner function"""
print("Before function execution...")
return func(*args, **kwargs)
return wrapper
@my_decorator
def say_hello():
"""A simple greeting function"""
print("Hello!")
print(say_hello.__name__) # say_hello (without @wraps, would show wrapper)
print(say_hello.__doc__) # A simple greeting function (without @wraps, would show decorator inner function)
Tip: Always add
@wrapswhen writing decorators. Without it, the decorated function's name and documentation are lost, making debugging difficult. Importwrapsfromfunctoolsand apply@wraps(func)to the inner wrapper function.
4. Parameterized Decorators
Sometimes the decorator itself needs parameters:
from functools import wraps
def repeat(times=2):
"""Repeat execution a specified number of times"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Zhang San")
Output:
Hello, Zhang San!
Hello, Zhang San!
Hello, Zhang San!
Example: Timing Decorator (Difficulty: Star-Star-Star)
import time
from functools import wraps
def timer(func):
"""Calculate function execution time"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[Timer] {func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
total = 0
for i in range(1000000):
total += i
return total
result = slow_function()
print(f"Result: {result}")
Output (timing varies by computer):
[Timer] slow_function took 0.0452 seconds
Result: 499999500000
5. Combining Multiple Decorators
A function can have multiple decorators stacked. Execution order is from bottom to top:
from functools import wraps
def bold(func):
@wraps(func)
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def hello():
return "Hello"
print(hello()) # <b><i>Hello</i></b>
# Execution order: first @italic, then @bold
# Equivalent to: hello = bold(italic(hello))
Tip: Multiple decorators execute from bottom to top. Think of the function being "wrapped" layer by layer — the bottom decorator is closest to the original function and executes first. Understanding this order keeps multiple decorators from causing confusion.
Common Use Cases
- Logging: Automatically record each function's calls and return values.
- Performance timing: Measure function execution time to identify bottlenecks.
- Permission checks: Web frameworks use
@login_requireddecorators to check if users are logged in. - Caching:
@functools.lru_cache()automatically caches function results. - Retry: Automatically retry a specified number of times on network request failure.
FAQ
*args and kwargs necessary in decorators?*args, kwargs to pass all arguments. If wrapping only functions with a specific signature, you can name specific parameters, but this is less general. Generic decorators almost always use *args, **kwargs.func parameter passed from the outer function.@property (attribute access), @staticmethod (static methods), @classmethod (class methods), @functools.lru_cache() (caching), @functools.wraps (preserving function info), @dataclass (data classes). All are in the standard library.Summary
- Closure: an inner function remembers the outer function's variables, even after the outer function has finished executing
- Decorator: a function that takes a function and returns a function; uses
@syntax sugar @wraps(func)preserves the original function's__name__and__doc__— always use it in decorators- Parameterized decorators: three levels of nesting — outer receives params, middle receives function, inner handles logic
- Multiple decorators are applied from bottom to top
- Decorators are "functions of functions" — enhancing functions without modifying original code
Exercises
-
Beginner (Difficulty: Star): Write a
print_calldecorator that prints "Calling xxx function" when the function executes. -
Intermediate (Difficulty: Star-Star): Write a
retry(max_attempts=3)decorator that automatically retries the specified number of times when a function raises an exception. If retries are exhausted, raise the final exception. Hint: Use a for loop + try-except in the wrapper. -
Advanced (Difficulty: Star-Star-Star): Write a parameterized decorator
cache_result(ttl=60)that caches the function's return value. Within the TTL seconds, calling with the same parameters returns the cached result; beyond the TTL, recompute the value.



