Python FAQ: Top Questions
27. What are Python decorators? How do they work?
A **decorator** in Python is a special type of function that takes another function as an argument, adds some functionality to it, and then returns the modified function. Decorators allow you to "wrap" a function or method, pre- or post-process its execution, or augment its behavior without permanently modifying its source code. They are a powerful and elegant way to apply cross-cutting concerns (like logging, timing, authentication, caching, etc.) to multiple functions.
The syntax for using a decorator is the `@` symbol followed by the decorator function's name, placed immediately above the function definition you want to decorate.
How Decorators Work (Conceptually):
At its core, a decorator is just syntactic sugar for the following:
# Original function
def my_function():
# ... some code ...
# Decoration using @syntax
@decorator_function
def my_function():
# ... some code ...
# Is equivalent to:
def my_function():
# ... some code ...
my_function = decorator_function(my_function)
Let's break down the mechanics:
- Decorator Function Definition: A decorator function typically takes a function as its argument.
- Inner Wrapper Function: Inside the decorator, an inner function (often called `wrapper` or `decorated_function`) is defined. This `wrapper` function is where the new functionality (the "decoration") is added. It usually calls the original function passed to the decorator.
- Returning the Wrapper: The decorator function returns this `wrapper` function.
- Assignment: When Python encounters the `@decorator_name` syntax, it executes the `decorator_name` function, passing the decorated function as an argument. The return value of the decorator (which is the `wrapper` function) then *replaces* the original function definition. So, whenever you call the decorated function, you're actually calling the `wrapper` function, which then handles the added logic and calls the original function.
This pattern makes functions first-class objects in Python very powerful, as they can be passed around, returned from other functions, and assigned to variables.
Benefits of Decorators:
- Code Reusability: Apply the same logic to multiple functions without duplicating code.
- Separation of Concerns: Keep core business logic separate from cross-cutting concerns like logging or authentication.
- Readability: The `@` syntax makes it clear that a function's behavior is being augmented.
- Flexibility: Easily add or remove functionality by simply adding or removing the decorator line.
Decorators can also take arguments themselves, making them even more versatile. This typically involves a "decorator factory" pattern where an outer function takes the arguments and returns the actual decorator function.
import time
import functools # Used for @functools.wraps
# --- Example 1: Simple Logging Decorator ---
print("--- Simple Logging Decorator ---")
def log_function_call(func):
"""
A simple decorator that logs when a function is called and its arguments.
"""
@functools.wraps(func) # Helps preserve original function's metadata
def wrapper(*args, **kwargs):
print(f"[{time.ctime()}] Calling function: {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs) # Call the original function
print(f"[{time.ctime()}] Function {func.__name__} finished. Result: {result}")
return result
return wrapper
@log_function_call
def add(a, b):
"""Adds two numbers."""
return a + b
@log_function_call
def greet(name):
"""Greets a person."""
return f"Hello, {name}!"
# Call the decorated functions
print(f"Result of add: {add(10, 5)}")
print(f"Result of greet: {greet('Alice')}")
print(f"\nOriginal add function name: {add.__name__}") # Should be 'add' due to @functools.wraps
# --- Example 2: Timing Decorator ---
print("\n--- Timing Decorator ---")
def timer(func):
"""
A decorator that measures the execution time of a function.
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Function {func.__name__!r} took {run_time:.4f} seconds to execute.")
return result
return wrapper
@timer
def long_running_task(n):
"""Simulates a task that takes some time."""
sum_val = 0
for i in range(n):
sum_val += i
time.sleep(0.1) # Simulate I/O delay
return sum_val
@timer
def quick_task():
"""A very quick task."""
return "Done quickly!"
long_running_task(1000000)
quick_task()
# --- Example 3: Decorator with Arguments (Decorator Factory) ---
print("\n--- Decorator with Arguments ---")
def repeat(num_times):
"""
A decorator factory that returns a decorator to repeat a function's execution.
"""
def decorator_repeat(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(num_times):
print(f"Repeating {func.__name__} (run {_ + 1}/{num_times})...")
results.append(func(*args, **kwargs))
return results # Return results of all runs
return wrapper
return decorator_repeat
@repeat(num_times=3) # Calling the decorator factory
def say_hello(name):
print(f"Hello, {name}!")
return f"Hello, {name}!"
@repeat(num_times=2)
def generate_random_number():
import random
num = random.randint(1, 100)
print(f"Generated: {num}")
return num
print("\nCalling say_hello:")
say_hello("World")
print("\nCalling generate_random_number:")
generate_random_number()
# --- Example 4: Chaining Decorators ---
print("\n--- Chaining Decorators ---")
# Apply both log_function_call and timer to one function
@log_function_call # This decorator runs first (further from function)
@timer # This decorator runs second (closer to function)
def complex_operation(x, y):
"""A complex operation."""
time.sleep(0.05)
return x * y + 100
print("\nCalling complex_operation:")
complex_operation(7, 8)
Explanation of the Example Code:
-
**`log_function_call` Decorator:**
- This decorator wraps the `add` and `greet` functions.
- When `add(10, 5)` is called, it's actually `wrapper(10, 5)` that executes.
- The `wrapper` prints a log message, calls the original `func` (which is `add` or `greet`), captures its result, prints another log message, and then returns the result.
- `@functools.wraps(func)` is important. Without it, `add.__name__` would incorrectly report `'wrapper'`. `wraps` copies over metadata like `__name__`, `__doc__`, etc., from the original function to the wrapper, making introspection easier.
-
**`timer` Decorator:**
- This decorator measures the execution time of the decorated function.
- It uses `time.perf_counter()` to get precise timestamps before and after `func(*args, **kwargs)` is called, then calculates and prints the duration.
- This is a common use case for decorators: profiling performance without altering the core logic.
-
**`repeat` Decorator with Arguments:**
- This demonstrates a "decorator factory." `repeat(num_times)` is not the decorator itself, but a function that *returns* the actual decorator (`decorator_repeat`).
- When you use `@repeat(num_times=3)`, Python first calls `repeat(3)`. This call returns the `decorator_repeat` function. Then, `decorator_repeat` (which now "remembers" `num_times=3` through closure) acts as the decorator for `say_hello`.
- The `wrapper` inside `decorator_repeat` then calls the original `say_hello` function `num_times` repeatedly.
-
**Chaining Decorators:**
- You can apply multiple decorators to a single function. The order matters!
- `@log_function_call` is applied first, then `@timer`. This means `log_function_call` wraps the function returned by `timer`.
- When `complex_operation` is called, `log_function_call`'s wrapper runs first, then it calls `timer`'s wrapper, which in turn calls the actual `complex_operation`. The output reflects this nesting.
These examples showcase the flexibility and power of decorators for cleanly adding functionality to functions and methods without modifying their intrinsic code, significantly improving code organization and reusability.