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.

PYTHON
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:

PYTHON
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:

TEXT
=== 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)

PYTHON
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"))
▶ Try it Yourself

Output:

TEXT
[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:

PYTHON
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 @wraps when writing decorators. Without it, the decorated function's name and documentation are lost, making debugging difficult. Import wraps from functools and apply @wraps(func) to the inner wrapper function.


4. Parameterized Decorators

Sometimes the decorator itself needs parameters:

PYTHON
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:

TEXT
Hello, Zhang San!
Hello, Zhang San!
Hello, Zhang San!

Example: Timing Decorator (Difficulty: Star-Star-Star)

PYTHON
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}")
▶ Try it Yourself

Output (timing varies by computer):

TEXT
[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:

PYTHON
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


FAQ

Q Are *args and kwargs necessary in decorators?
A If your decorator wraps functions with an unknown number of parameters, yes — use *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.
Q What's the relationship between decorators and closures?
A Decorators are implemented using closures. A closure is "a function that remembers external variables." Decorators leverage closures so the inner wrapper function can access the func parameter passed from the outer function.
Q What useful decorators come with Python?
A @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


Exercises

  1. Beginner (Difficulty: Star): Write a print_call decorator that prints "Calling xxx function" when the function executes.

  2. 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.

  3. 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.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏