Python tutorials > Advanced Python Concepts > Decorators > What are decorators?
What are decorators?
Decorators are a powerful and expressive feature in Python that allows you to modify or enhance the behavior of functions or methods. They provide a way to wrap functions with additional functionality without permanently modifying their original code. In essence, a decorator is syntactic sugar that simplifies the process of applying higher-order functions.
Basic Decorator Structure
This snippet demonstrates the fundamental structure of a decorator. The When my_decorator
is a function that takes another function (func
) as an argument. Inside my_decorator
, a nested function called wrapper
is defined. wrapper
contains the code that will be executed before and after the original function (func
) is called. Finally, my_decorator
returns the wrapper
function.@my_decorator
syntax is a shorthand way of applying the decorator. It's equivalent to writing say_hello = my_decorator(say_hello)
.say_hello()
is called, it's actually calling the wrapper
function returned by my_decorator
, which then executes the "before" code, the original say_hello()
function, and the "after" code.
def my_decorator(func):
def wrapper():
# Code to execute before the original function
print("Before function call")
func()
# Code to execute after the original function
print("After function call")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Concepts Behind the Snippet
The core concept is that functions are first-class citizens in Python. This means you can treat functions like any other object: pass them as arguments, return them from other functions, and assign them to variables. Decorators leverage this to modify function behavior. The @
symbol, known as the 'at' symbol or decorator syntax, makes using decorators more readable and concise.
Decorator with Arguments
This example shows a decorator that accepts arguments. The When calling repeat
function takes num_times
as an argument and returns another function, decorator_repeat
, which then takes the function to be decorated. The wrapper
function now uses *args
and **kwargs
to handle any number of positional and keyword arguments that the decorated function might accept. This makes the decorator more versatile.@repeat(num_times=3)
, you're essentially calling greet = repeat(num_times=3)(greet)
def repeat(num_times):
def decorator_repeat(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("Alice")
Understanding *args and **kwargs
Using both *args
allows a function to accept any number of positional arguments. These arguments are collected into a tuple named args
.**kwargs
allows a function to accept any number of keyword arguments. These arguments are collected into a dictionary named kwargs
.*args
and **kwargs
in a decorator ensures that it can be used with functions that have different argument signatures.
Real-Life Use Case: Logging
This demonstrates a practical use case: logging function calls. The log_calls
decorator logs the function name, arguments, and return value. functools.wraps
is important here. It updates the wrapper function to look like the decorated function (add
in this case). This preserves the original function's __name__
, __doc__
, etc., which is essential for introspection and debugging.
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} returned: {result}")
return result
return wrapper
@log_calls
def add(x, y):
return x + y
add(5, 3)
Importance of functools.wraps
Without functools.wraps
, the decorated function's metadata (like its name and docstring) would be overwritten by the wrapper function's metadata. This can make debugging and documentation generation difficult. functools.wraps
copies the original function's metadata to the wrapper, making it appear as if the original function is still being called.
Best Practices
functools.wraps
: Always use functools.wraps
to preserve the original function's metadata.
Interview Tip
Be prepared to explain what decorators are, how they work, and why they are useful. Be able to write a simple decorator on the spot. Understanding functools.wraps
and its importance is a plus.
When to Use Them
Use decorators when you need to add functionality to a function or method without modifying its core logic. Common use cases include:
Memory Footprint
Decorators can add a slight overhead due to the extra function call. However, this overhead is usually negligible. Overusing decorators with excessively complex logic inside them may impact performance, but well-designed decorators are generally efficient.
Alternatives
Alternatives to decorators include:
Pros
Cons
functools.wraps
helps mitigate this).
FAQ
-
What is the difference between a decorator and a wrapper?
A wrapper is a function that calls another function. A decorator is a way to apply a wrapper to a function using the
@
syntax. Decorators are syntactic sugar that makes using wrappers more convenient and readable. -
Can I apply multiple decorators to a single function?
Yes, you can apply multiple decorators to a single function. The decorators are applied in the order they appear, from top to bottom.
-
How do I debug a decorated function?
Use a debugger like
pdb
or the debugger built into your IDE. Step through the wrapper function to understand the flow of execution. Make sure you're usingfunctools.wraps
to preserve the original function's metadata, which will help with debugging.