Python FAQ: Top Questions
10. What are Python decorators?
Python **decorators** are a powerful and elegant way to modify or enhance the behavior of functions or methods without permanently altering their source code. They are essentially **wrappers** that allow you to "decorate" a function, adding new functionality before or after the original function executes. This concept is built upon Python's ability to treat functions as first-class objects, meaning they can be passed as arguments, returned from other functions, and assigned to variables.
### How Decorators Work:
A decorator is a function that takes another function as an argument, adds some functionality, and returns a new function (or a modified version of the original). The `@` syntax is just syntactic sugar for a common pattern:
@decorator_function
def original_function():
# ... code ...
is equivalent to:
def original_function():
# ... code ...
original_function = decorator_function(original_function)
### Key Concepts and Benefits:
- Functions as First-Class Objects: Python's ability to treat functions like any other object (assign them to variables, pass them as arguments, return them from other functions) is the foundation of decorators.
- Higher-Order Functions: Decorators are a type of higher-order function—a function that takes one or more functions as arguments or returns a function as its result.
- Code Reusability: Decorators promote the **Don't Repeat Yourself (DRY)** principle. Common functionalities (like logging, timing, authentication, caching, error handling) can be implemented once as a decorator and then applied to multiple functions without duplicating code.
- Separation of Concerns: They help separate cross-cutting concerns (e.g., logging) from the core business logic of a function, leading to cleaner and more modular code.
- Readability and Maintainability: Using decorators with the `@` syntax makes code more readable by clearly indicating that a function's behavior is being augmented. It also makes maintenance easier, as changes to the added functionality only need to be made in one place (the decorator definition).
- Preserving Original Functionality: Decorators typically wrap the original function in an inner function, allowing the original function's core logic to execute while adding surrounding features.
Decorators are widely used in Python frameworks (e.g., `@app.route` in Flask, `@login_required` in Django) and in various utility libraries to add non-core functionalities.
import time
import functools
# --- Example 1: A simple timing decorator ---
# A decorator function that takes a function 'func' as an argument
def timer_decorator(func):
# The 'wrapper' function will replace the original function.
# It must accept any arguments (*args, **kwargs) that the original function accepts.
@functools.wraps(func) # Use functools.wraps to preserve original function's metadata (name, docstring)
def wrapper(*args, **kwargs):
start_time = time.perf_counter() # Record start time
result = func(*args, **kwargs) # Call the original function
end_time = time.perf_counter() # Record end time
run_time = end_time - start_time
print(f"Function '{func.__name__}' took {run_time:.4f} seconds to execute.")
return result # Return the result of the original function
return wrapper # The decorator returns the new 'wrapper' function
# Apply the decorator using the '@' syntax
@timer_decorator
def long_running_task(n):
"""A sample function that simulates a long-running task."""
print(f"Running long_running_task with n={n}...")
time.sleep(n) # Simulate work
print(f"long_running_task finished.")
return f"Task completed in {n} seconds."
@timer_decorator
def short_task():
"""A quick task."""
print("Running short_task...")
return "Short task done."
# Call the decorated functions
print("\n--- Calling decorated functions ---")
long_running_task(2)
print(long_running_task(0.5)) # Also captures the return value
print("-" * 20)
short_task()
print(short_task()) # Also captures the return value
print("---------------------------------")
# --- Example 2: A logging decorator ---
def log_calls(func):
"""A decorator that logs function calls with their arguments."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Log before the function call
print(f"LOG: Calling function '{func.__name__}' with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
# Log after the function call
print(f"LOG: Function '{func.__name__}' returned: {result}")
return result
return wrapper
@log_calls
def add_numbers(a, b):
"""Adds two numbers."""
return a + b
@log_calls
def concatenate_strings(s1, s2, separator=" "):
"""Concatenates two strings with a separator."""
return s1 + separator + s2
print("\n--- Calling functions with logging decorator ---")
add_numbers(5, 7)
print("-" * 10)
concatenate_strings("Hello", "World", separator="-")
print("-" * 10)
concatenate_strings("First", "Last") # Using default separator
print("---------------------------------------------")
Explanation of the Example Code:
-
**`timer_decorator`:**
- This decorator is defined as a function `timer_decorator` that takes another function `func` (the function to be decorated) as its argument.
- Inside `timer_decorator`, it defines an inner function called `wrapper`. This `wrapper` function is what actually replaces `func` when the decorator is applied.
- `wrapper` captures the `start_time` before calling the `func(*args, **kwargs)` (the original function with its arguments). It then captures the `end_time` and prints the execution duration. Finally, it returns the `result` from the original function.
- `@functools.wraps(func)` is crucial. It copies the `func`'s metadata (like `__name__` and `__doc__`) to the `wrapper` function, ensuring that introspection tools correctly identify the decorated function, not the wrapper.
- When `@timer_decorator` is placed above `long_running_task` and `short_task`, it's equivalent to `long_running_task = timer_decorator(long_running_task)`. Now, when `long_running_task()` is called, it's actually the `wrapper` function that executes, performing the timing before and after running the original task.
-
**`log_calls`:**
- This decorator operates similarly to `timer_decorator` but adds logging functionality.
- The `wrapper` function prints messages before and after the original function call, showing the arguments passed and the value returned.
- Applying `@log_calls` to `add_numbers` and `concatenate_strings` automatically adds this logging behavior to their executions without modifying their core logic.
These examples illustrate how decorators provide a clean, Pythonic, and reusable way to add cross-cutting concerns (like timing or logging) to functions without cluttering their main implementation. This leads to more modular, readable, and maintainable codebases.