Mastering Python Decorators: Creative Tricks to Simplify Your Code
Learn how to master Python decorators with beginner-friendly tricks to write simpler, cleaner, and more reusable code.
Python decorators are a powerful tool that allows you to modify the behavior of functions or methods. If you're new to decorators, they might seem confusing at first, but once you understand them, they can greatly simplify your code and help you follow the DRY (Don't Repeat Yourself) principle. In this tutorial, we'll explore what decorators are, how to create your own, and some creative tricks to use them effectively.
At a basic level, a decorator is a function that takes another function and extends its behavior without explicitly modifying it. This means you can add features like logging, timing, or access control in a clean and reusable way.
Let's start with a simple example of a decorator to print a message before and after a function runs.
def my_decorator(func):
def wrapper():
print("Before the function runs")
func()
print("After the function runs")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()In this code, `my_decorator` is a function that takes `func` as input and defines a nested `wrapper` function that runs code before and after calling `func`. The `@my_decorator` line is syntactic sugar for `say_hello = my_decorator(say_hello)`. When you call `say_hello()`, it actually runs `wrapper`.
The output will be: Before the function runs Hello! After the function runs
### Handling Functions with Arguments Most functions take arguments, so your decorator should too. You can do this using `*args` and `**kwargs` to accept any number of positional and keyword arguments.
def decorator_with_args(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with arguments {args} and {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@decorator_with_args
def add(a, b):
return a + b
add(5, 3)This decorator prints the function’s name, the arguments it received, and the result it returned. This is useful for debugging or logging purposes.
### Preserving Function Metadata When you decorate a function, Python replaces the original function with the wrapper, which can hide useful information like the function’s name and docstring. To fix this, use the `functools.wraps` decorator inside your wrapper.
import functools
def preserve_metadata(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Running {func.__name__}")
return func(*args, **kwargs)
return wrapper
@preserve_metadata
def greet(name):
"""Returns a greeting message"""
return f"Hello, {name}!"
print(greet.__name__) # Output: greet
print(greet.__doc__) # Output: Returns a greeting message### Creative Trick 1: Timing Your Functions Let’s create a decorator to measure how long a function takes to run. This is handy for optimizing your code.
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds to run")
return result
return wrapper
@timer
def countdown(n):
while n > 0:
n -= 1
countdown(1000000)### Creative Trick 2: Access Control Decorator You can use decorators to control access to certain functions. For example, let's make a simple user authorization decorator.
def requires_admin(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get('is_admin', False):
print("Access denied! Admins only.")
return None
return func(user, *args, **kwargs)
return wrapper
@requires_admin
def delete_database(user):
print("Database deleted!")
user = {'username': 'jane', 'is_admin': False}
admin = {'username': 'john', 'is_admin': True}
delete_database(user) # Access denied! Admins only.
delete_database(admin) # Database deleted!### Summary Decorators are a versatile feature in Python that can help you write more readable, maintainable, and reusable code. Start by understanding the basic decorator structure, using `*args` and `**kwargs` to handle arbitrary arguments, preserving function metadata with `functools.wraps`, and applying creative use cases like timing and access control. With practice, you'll find many opportunities to simplify your code with decorators!
Happy coding!