Python Decorators Explained: From Basics to Advanced Patterns
Decorators are one of Python's most powerful and distinctive features. They allow you to modify or enhance the behavior of functions or classes without directly changing their source code.
While they can look intimidating at first (what's with the @ symbol?), they are surprisingly simple once you understand that they are just functions that take other functions as input.
What is a Decorator?
At its core, a decorator is a wrapper. It takes a function, adds some functionality before or after it runs, and returns the modified function.
The Basic Syntax
Here is the manual way to apply a decorator versus the syntactic sugar using the @ symbol. Both do exactly the same thing.
# The manual way
def my_decorator(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)
# The Pythonic way (Syntactic Sugar)
@my_decorator
def say_hello():
print("Hello!")Real-World Examples
Let's look at three practical scenarios where decorators make your code cleaner and more efficient.
1. Timing Execution
A common use case is measuring how long a function takes to run. Instead of adding timing code to every function, you can write one decorator.
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return result
return wrapper
@timer
def heavy_computation():
sum([i**2 for i in range(1000000)])
heavy_computation()
# Output: Finished 'heavy_computation' in 0.3452 secs2. Retry Logic
When dealing with network requests or APIs, failures happen. A retry decorator can automatically try the operation again if it fails.
import random
import time
def retry(max_attempts=3, delay=1):
def decorator(func):
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
raise e
print(f"Attempt {attempts} failed. Retrying...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3)
def unstable_network_call():
if random.random() < 0.7:
raise ConnectionError("Network fail")
return "Success!"3. Authentication Check
In web frameworks like Flask or Django, decorators are often used to enforce permissions.
def require_admin(func):
def wrapper(user, *args, **kwargs):
if not user.get('is_admin'):
raise PermissionError("User must be an admin to access this resource")
return func(user, *args, **kwargs)
return wrapper
@require_admin
def delete_database(user):
print("Database deleted!")
user = {'username': 'navin', 'is_admin': False}
# delete_database(user) # Raises PermissionErrorBuilt-in Decorators
Python comes with several useful decorators built-in.
@property: Allows you to access a method like an attribute.@staticmethod: Defines a method that doesn't need access to the instance (self) or class (cls).@classmethod: Defines a method that receives the class (cls) as the first argument instead of the instance.@functools.lru_cache: Automatically memoizes (caches) the results of a function based on its arguments.
Best Practices
When writing your own decorators, keep these tips in mind:
- Use
functools.wraps: This ensures your decorated function retains its original name and docstring. - Handle
*argsand**kwargs: Make your wrapper function accept arbitrary arguments so it can work with any function signature. - Keep them focused: Do one thing well. If your decorator is doing logging, timing, and error handling, split it up.
Experiment with Decorators
The best way to learn is by doing. Try creating your own decorators in CoilPad and see how they transform your code instantly.
Download CoilPad