Python Decorators
A decorator in Python is a function that takes another function as an argument and extends or alters its behaviour. Don’t worry if that sounds a bit of a mouthful, all will become clear very soon!
Decorators are often used for logging, access control, and performance measurement, but there are all kinds of application.
The most common way to apply a decorator is by using the @decorator_name
syntax, which is ‘syntactic sugar’ for passing a function to a decorator.
Table of Contents
Basic Decorator Example
It’s often easiest to explain with an example, so here is a simple decorator that prints a message before and after calling a function:
def my_decorator(func):
def wrapper():
print("Before the function call")
func()
print("After the function call")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
Before the function call
Hello!
After the function call
@my_decorator
: This applies themy_decorator
function tosay_hello()
. It’s equivalent tosay_hello = my_decorator(say_hello)
.wrapper()
: This is the inner function that wraps the original function (say_hello()
). It adds the extra behavior (printing “Before” and “After”).
Anatomy of a Decorator
Decorators typically have three components:
- The outer function: This takes the target function as an argument.
- The wrapper function: This is the inner function that wraps the original function and adds new functionality.
- The return statement: The outer function returns the wrapper function, which replaces the original function.
Example:
def decorator(func):
def wrapper():
# Code to run before the function
print("Before the function is called")
func() # Call the original function
# Code to run after the function
print("After the function is called")
return wrapper
Functions as First-Class Objects in Python
To understand decorators, it’s essential to know that functions are first-class objects in Python. This means you can:
- Assign functions to variables.
- Pass functions as arguments to other functions.
- Return functions from other functions.
Example:
def greet():
return "Hello!"
# Assign a function to a variable
say_hello = greet
print(say_hello()) # Output: Hello!
This behavior enables decorators, as they rely on passing functions as arguments and returning modified versions.
Applying Multiple Decorators
You can apply multiple decorators to a function by stacking them. Decorators are applied from top to bottom.
Example:
def decorator_one(func):
def wrapper():
print("Decorator One")
func()
return wrapper
def decorator_two(func):
def wrapper():
print("Decorator Two")
func()
return wrapper
@decorator_one
@decorator_two
def say_hello():
print("Hello!")
say_hello()
Output:
Decorator One
Decorator Two
Hello!
Here, decorator_two
is applied first, followed by decorator_one
.
Passing Arguments to Decorators
So far, we’ve seen decorators that wrap functions with no arguments. To create a decorator for a function that takes arguments, you need to modify the wrapper to accept *args
and **kwargs
. These allow the wrapper to handle any number of positional and keyword arguments.
Example: A Decorator with Arguments
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function call")
result = func(*args, **kwargs)
print("After the function call")
return result
return wrapper
@my_decorator
def greet(name, age):
print(f"Hello, {name}. You are {age} years old.")
greet("Alice", 30)
Output:
Before the function call
Hello, Alice. You are 30 years old.
After the function call
*args
: Captures all positional arguments.**kwargs
: Captures all keyword arguments.- The
wrapper()
function now accepts any arguments passed togreet()
and forwards them to the original function.
Returning Values from Decorated Functions
If the original function returns a value, the decorator should also return that value to maintain the original behavior.
Example: Returning Values
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function call")
result = func(*args, **kwargs)
print("After the function call")
return result # Ensure the return value is passed back
return wrapper
@my_decorator
def add(a, b):
return a + b
result = add(3, 4)
print(f"Result: {result}")
Output:
Before the function call
After the function call
Result: 7
Here, the decorator ensures that the return value from add()
is passed back by the wrapper.
Real-World Use Cases for Decorators
Decorators are used in a variety of scenarios to simplify and reuse common logic. Let’s look at some common use cases:
1. Logging
Logging is a common use case for decorators. You can create a decorator that logs the execution of a function.
Example:
def log_execution(func):
def wrapper(*args, **kwargs):
print(f"Executing {func.__name__} with arguments {args} and {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_execution
def multiply(a, b):
return a * b
multiply(2, 3)
Output:
Executing multiply with arguments (2, 3) and {}
multiply returned 6
2. Timing Execution
You can measure the time it takes for a function to execute using a decorator.
Example:
import time
def time_it(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds to execute")
return result
return wrapper
@time_it
def slow_function():
time.sleep(2)
return "Finished"
slow_function()
Output:
slow_function took 2.0004 seconds to execute
3. Access Control (Authorization)
Decorators can be used to restrict access to certain functions, such as checking user permissions before allowing a function to run.
Example:
def requires_admin(func):
def wrapper(user):
if user != "admin":
print("Access denied. Admins only.")
return
return func(user)
return wrapper
@requires_admin
def delete_database(user):
print("Database deleted.")
delete_database("guest") # Access denied
delete_database("admin") # Database deleted
Decorators with Arguments
Sometimes you need a decorator that accepts its own arguments. This requires adding an extra layer of functions.
Example: Decorator with Arguments
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
Output:
Hello, Alice!
Hello, Alice!
Hello, Alice!
Here, the repeat()
decorator takes an argument (times
) that controls how many times the greet()
function is called.
Preserving Function Metadata with functools.wraps
When a function is wrapped by a decorator, its metadata (such as the function name and docstring) is replaced by the wrapper’s metadata. To preserve the original function’s metadata, you can use the functools.wraps
decorator inside your wrapper function.
Example:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet the person by name."""
print(f"Hello, {name}!")
print(greet.__name__) # Output: greet
print(greet.__doc__) # Output: Greet the person by name.
By using @functools.wraps
, the original function’s name and docstring are preserved.
Key Concepts Recap
In this lesson, we covered:
- The basics of Python decorators and how they work.
- How to create decorators to modify the behavior of functions.
- Real-world use cases for decorators, such as logging, timing, and access control.
- How to use
*args
and**kwargs
to handle arguments in decorators. - How to use
functools.wraps
to preserve function metadata.
Decorators are a powerful tool that can enhance code reusability and maintainability. With this knowledge, you can start creating your own decorators to streamline repetitive logic in your Python projects.
Exercises
- Write a decorator that logs the arguments and return value of any function it is applied to.
- Create a decorator that times how long a function takes to execute and prints the result.
- Write a decorator that checks if a user is authorized to run a function by comparing a “role” argument to a predefined list of allowed roles.
- Create a decorator that retries calling a function a specified number of times if it raises an exception.
FAQ
Q1: What exactly is a decorator in Python?
A1: A decorator in Python is a function that takes another function (or method) as an argument, adds some additional functionality to it, and returns a new function or the original function with the added behavior. The syntax @decorator_name
is used to apply the decorator to a function. Decorators are commonly used for logging, timing, validation, and access control.
Q2: Can a decorator modify the arguments passed to a function?
A2: Yes, a decorator can modify the arguments before passing them to the original function. This is done inside the wrapper function by manipulating *args
and **kwargs
.
Example:
def modify_args(func):
def wrapper(*args, **kwargs):
# Modify the arguments
args = (arg.upper() if isinstance(arg, str) else arg for arg in args)
return func(*args, **kwargs)
return wrapper
@modify_args
def greet(name):
print(f"Hello, {name}!")
greet("alice") # Output: Hello, ALICE!
Q3: Can a decorator be applied to a function with a return value?
A3: Yes, decorators can be used with functions that return values. In fact, it’s important to ensure the wrapper function in the decorator properly returns the value from the decorated function so that the function behaves as expected.
Example:
def add_logging(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result # Ensure the result is returned
return wrapper
@add_logging
def add(a, b):
return a + b
add(3, 4) # Output: add returned 7
Q4: Can I apply multiple decorators to a single function?
A4: Yes, you can apply multiple decorators to a function. The decorators are applied from top to bottom, meaning the bottom-most decorator is applied first, and then the one above it, and so on.
Example:
def decorator_one(func):
def wrapper(*args, **kwargs):
print("Decorator One")
return func(*args, **kwargs)
return wrapper
def decorator_two(func):
def wrapper(*args, **kwargs):
print("Decorator Two")
return func(*args, **kwargs)
return wrapper
@decorator_one
@decorator_two
def say_hello():
print("Hello!")
say_hello()
Output:
Decorator Two
Decorator One
Hello!
Q5: What is functools.wraps
, and why should I use it?
A5: The functools.wraps
decorator is used inside a wrapper function to preserve the original function’s metadata (like its name and docstring). Without functools.wraps
, the name and docstring of the original function will be replaced by the wrapper function’s name and docstring, which can cause confusion when debugging or using reflection.
Example:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_function():
"""This is my function."""
print("Function executed")
print(my_function.__name__) # Output: my_function
print(my_function.__doc__) # Output: This is my function.
Q6: How do I create a decorator that takes its own arguments?
A6: To create a decorator that takes its own arguments, you need to add an extra layer of functions. The outermost function accepts the decorator’s arguments, the next layer is the actual decorator, and the innermost function is the wrapper.
Example:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
This example calls the greet
function 3 times because of the repeat(3)
decorator.
Q7: Can decorators be applied to class methods or static methods?
A7: Yes, decorators can be applied to class methods and static methods. They work the same way as for regular functions, but you need to ensure that the correct method type (e.g., self
for instance methods) is passed.
Example for a class method:
def log_method_call(func):
def wrapper(self, *args, **kwargs):
print(f"Calling method {func.__name__}")
return func(self, *args, **kwargs)
return wrapper
class MyClass:
@log_method_call
def say_hello(self, name):
print(f"Hello, {name}!")
obj = MyClass()
obj.say_hello("Alice")
Q8: How do I decorate a function that takes an unknown number of arguments?
A8: To handle a variable number of arguments, use *args
and **kwargs
in your decorator’s wrapper function. This allows you to pass any number of positional and keyword arguments to the decorated function.
Example:
def log_arguments(func):
def wrapper(*args, **kwargs):
print(f"Arguments: {args}, {kwargs}")
return func(*args, **kwargs)
return wrapper
@log_arguments
def add(a, b, c=0):
return a + b + c
add(1, 2) # Output: Arguments: (1, 2), {}
add(1, 2, c=3) # Output: Arguments: (1, 2), {'c': 3}
Q9: Can I use a decorator to modify the return value of a function?
A9: Yes, a decorator can modify or alter the return value of a function before passing it back to the caller. This is done within the wrapper function.
Example:
def double_return(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result * 2 # Modify the return value
return wrapper
@double_return
def get_number():
return 5
print(get_number()) # Output: 10
Q10: Can I remove a decorator after it’s applied to a function?
A10: Once a decorator is applied to a function, it wraps the original function, so you can’t directly “remove” a decorator. However, if you need to use the original function without the decorator, you can access it by keeping a reference to the undecorated function before applying the decorator.
Example:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Decorator applied")
return func(*args, **kwargs)
return wrapper
def my_function():
print("Original function")
# Keep a reference to the original function
original_function = my_function
# Apply the decorator
my_function = my_decorator(my_function)
my_function() # Output: Decorator applied, Original function
original_function() # Output: Original function (without decorator)
In this example, you keep a reference to the undecorated my_function
and use it later if needed.