Python decorators are a special and powerful feature that lets you change or add to how functions or methods work.
Even though they're really useful, decorators can be confusing, especially for people who are new to Python or don't know much about functional programming.
This article will make Python decorators easier to understand by explaining what they are, when you should use them, and showing some advanced examples.
What is a Decorator?
In Python, a decorator is a special kind of function that lets you change or improve how another function or method works.
Decorators make it easy to add features like logging, authentication, caching, and more, without changing the original code directly.
They help keep your code clean, easy to read, and reusable.
Decorators are useful when you want to add extra functionality to existing code without altering its structure.
Basic Syntax of a Decorator
To use a decorator with a function, you place the decorator function's name above the function you want to modify, with the @ symbol in front of it. Hereβs a simple example:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
In this example:
my_decorator
is the function that acts as a decorator.wrapper
is an inner function that wraps around the originalsay_hello
function.- The line
@my_decorator
is a shorthand way to writesay_hello = my_decorator(say_hello)
.
When you call say_hello()
, it doesn't directly run the say_hello
function. Instead, it calls the wrapper
function, which then calls say_hello
inside it. The output will be:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
When to Use Decorators
Decorators are especially helpful in these situations:
- Cross-Cutting Concerns: Tasks like logging, authentication, and timing operations often need to be done in many different functions. Instead of repeating the same code, you can put these tasks into decorators.
- Code Reusability: With decorators, you can write code that can be used again and again for many functions without copying and pasting.
- Separation of Concerns: Decorators help keep business logic separate from other tasks, making the code cleaner and easier to manage.
- DRY Principle: They follow the "Don't Repeat Yourself" rule by letting you reuse the same functions in different parts of your code.
Get "Python's Magic Methods - Beyond __init__ and __str__"
Magic methods are not just syntactic sugar, they're powerful tools that can significantly improve the functionality and performance of your code. With this book, you'll learn how to use these tools correctly and unlock the full potential of Python.
Advanced Use Cases for Python Decorators
Now that we have a basic understanding of what decorators are and when to use them, let's look at some advanced ways to use them.
#1 - Changing Function Arguments and Return Values
Decorators can be used to change the inputs given to a function or the value it returns.
This is especially helpful when you need to enforce rules or change data in some way:
def enforce_types(func):
def wrapper(*args, **kwargs):
new_args = []
for arg in args:
if isinstance(arg, int):
new_args.append(float(arg)) # Convert int to float
else:
new_args.append(arg)
return func(*new_args, **kwargs)
return wrapper
@enforce_types
def add(a, b):
return a + b
print(add(2, 3)) # Outputs 5.0
In this example, the enforce_types
decorator changes integer arguments into floats before sending them to the add
function.
#2 - Saving Function Results
Caching is a common way to make your code run faster.
It stores the results of expensive function calls so that if the same function is called again with the same arguments, it can quickly return the saved result instead of doing the calculation again:
from functools import lru_cache
@lru_cache(maxsize=32)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # Outputs 55
In this example, the lru_cache
decorator from the functools
module saves the results of the fibonacci
function.
This improves performance by avoiding unnecessary calculations.
#3 - Authorization and Authentication
Decorators are often used to enforce security checks in web applications.
Below is a simple example where we check if a user is logged in before letting them do something important:
def requires_authentication(func):
def wrapper(user, *args, **kwargs):
if not user.get('authenticated', False):
raise PermissionError("User is not authenticated.")
return func(user, *args, **kwargs)
return wrapper
@requires_authentication
def get_secret_data(user):
return "Secret Data"
user = {'name': 'John', 'authenticated': True}
print(get_secret_data(user)) # Outputs "Secret Data"
user = {'name': 'Jane', 'authenticated': False}
print(get_secret_data(user)) # Raises PermissionError
This requires_authentication
decorator checks if the user is logged in before allowing them to access the get_secret_data
function.
#4 - Retry Logic
Sometimes, you need to try again when a function fails due to temporary issues, like a network timeout.
Decorators are a great way to include this retry logic:
import time
import random
def retry(max_retries=3, delay=1):
def decorator(func):
def wrapper(*args, **kwargs):
retries = 0
while retries < max_retries:
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error: {e}. Retrying in {delay} seconds...")
time.sleep(delay)
retries += 1
return func(*args, **kwargs)
return wrapper
return decorator
@retry(max_retries=5, delay=2)
def unstable_operation():
if random.choice([True, False]):
raise ValueError("Random error!")
return "Success!"
print(unstable_operation())
# Outputs:
# Error: Random error!. Retrying in 2 seconds...
# Error: Random error!. Retrying in 2 seconds...
# Error: Random error!. Retrying in 2 seconds...
# Success!
Here, the retry
decorator will try to run the unstable_operation
function up to 5 times, waiting 2 seconds between each try.
If there's an error, the function will keep trying until it either succeeds or reaches the maximum number of tries.
#5 - Timing Functions
Decorators can be used to measure how long it takes for a function to run.
This is really useful for testing and monitoring performance:
import time
def timing(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
@timing
def slow_function():
time.sleep(2)
return "Finished!"
print(slow_function())
# Outputs:
# slow_function took 2.0004 seconds to execute.
# Finished!
The timing
decorator calculates and prints how long the slow_function
takes to run.
When slow_function
is called, it sleeps for 2 seconds, and the decorator measures this time and prints it afterward.
#6 - Decorating Classes and Methods
Decorators can also be used on class methods.
This lets you enforce certain behaviors or change methods at the class level:
This article is for paid members only
To continue reading this article, upgrade your account to get full access.
Subscribe NowAlready have an account? Sign In