Python tutorials > Advanced Python Concepts > Decorators > What are function decorators?
What are function decorators?
Decorators are a powerful and elegant feature in Python that allows you to modify or enhance functions and methods in a clean and readable way. They provide a way to add functionality to existing functions without modifying their core logic. Think of them as wrappers that enhance the behavior of a function before, after, or during its execution. Decorators are heavily used in frameworks and libraries for tasks like logging, authentication, timing, and caching.
Basic Structure of a Decorator
The code above showcases the basic structure of a decorator in Python. The When my_decorator
is a decorator function that takes another function (func
) as an argument. It defines an inner function called wrapper
. The wrapper
function does the following:
func
) using func(*args, **kwargs)
. The *args
and **kwargs
allow the decorator to work with functions that accept any number of positional and keyword arguments.my_decorator
function returns the wrapper
function. The @my_decorator
syntax (also called 'syntactic sugar') is a convenient way to apply the decorator to the say_hello
function. It's equivalent to say_hello = my_decorator(say_hello)
.say_hello("Alice")
is called, it's actually calling the wrapper
function, which then executes the pre-call code, calls the original say_hello
function, executes the post-call code, and returns the result.
def my_decorator(func):
def wrapper(*args, **kwargs):
# Code to execute before calling the original function
print("Before the function call.")
result = func(*args, **kwargs) # Call the original function
# Code to execute after calling the original function
print("After the function call.")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
Concepts Behind the Snippet
Understanding decorators requires grasping these core concepts:
wrapper
function in our decorator is a closure.
Real-Life Use Case: Logging
This example demonstrates a decorator for logging the execution time of a function. log_execution_time
measures how long process_data
takes to run and prints the result. functools.wraps(func)
is important for preserving the original function's metadata (name, docstring, etc.). Without it, func.__name__
would be 'wrapper' instead of 'process_data'.
import functools
import time
def log_execution_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
execution_time = end_time - start_time
print(f"{func.__name__} executed in {execution_time:.4f} seconds")
return result
return wrapper
@log_execution_time
def process_data(data):
time.sleep(2) # Simulate some processing time
return len(data)
result = process_data([1, 2, 3, 4, 5])
print(f"Result: {result}")
Best Practices
Here are some best practices to keep in mind when working with decorators:
functools.wraps
: Always use functools.wraps
to preserve the original function's metadata.*args
and **kwargs
.
Interview Tip
When discussing decorators in an interview, be prepared to explain the following:
functools.wraps
.
When to Use Them
Decorators are useful in the following scenarios:
Memory Footprint
Decorators can add a small overhead to the memory footprint. This is because they introduce an extra function call (the wrapper
function). However, the overhead is typically negligible compared to the overall memory usage of the application. It's only a concern if you are using decorators on a very large number of functions or if your application is extremely memory-constrained. The primary memory usage comes from keeping both the original and wrapped functions in memory.
Alternatives
While decorators are a powerful tool, there are alternative approaches for achieving similar results:
with
statement) can be used for certain tasks, such as managing resources (e.g., opening and closing files).
Pros of Using Decorators
Here are some advantages of using decorators:
@decorator
syntax is concise and elegant.
Cons of Using Decorators
Here are some potential drawbacks of using decorators:
FAQ
-
What is
functools.wraps
and why is it important?
functools.wraps
is a decorator itself that updates the wrapper function to look like the wrapped function. It copies the wrapped function's metadata (name, docstring, etc.) to the wrapper function. This is important for introspection, debugging, and documentation. Without it, tools like help() and debuggers might show information about the wrapper function instead of the original function. -
How do I pass arguments to a decorator?
To pass arguments to a decorator, you need to create a decorator factory – a function that returns a decorator. For example:
def repeat(num_times): def decorator_repeat(func): @functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(num_times): result = func(*args, **kwargs) return result return wrapper return decorator_repeat @repeat(num_times=3) def greet(name): print(f"Hello, {name}!") greet("World")
In this example,
repeat
is the decorator factory, which takes the number of repetitions as an argument and returns the actual decorator functiondecorator_repeat
. -
Are decorators only for functions? Can they be used on classes?
While the examples often show function decorators, decorators can also be used on classes. A class decorator typically modifies the class definition or adds attributes/methods to the class.