Decorators are one of Python's most elegant and powerful features. They allow you to modify or extend the behavior of functions without changing their code. Decorators are used throughout Python's standard library and popular frameworks, making them essential to understand.
In this lesson, you'll learn what decorators are, how to create your own, and how to use them for common tasks like timing functions, logging, validation, and caching. By the end, you'll be able to write decorators that make your code more maintainable and reusable.
What You'll Learn
- Understanding decorators and their syntax
- Creating simple decorators
- Decorators with arguments
- Multiple decorators
- Built-in decorators (@property, @staticmethod, @classmethod)
- Practical decorator examples (timing, logging, validation)
Understanding Decorators
A decorator is a function that takes another function and extends its behavior without modifying it. The @ symbol is syntactic sugar that makes decorators easy to use:
# Simple decorator 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()
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.
Without the @ syntax, you'd write:
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)
say_hello()
Creating Simple Decorators
Let's create a practical decorator that times function execution:
import time
def timer(func):
"""Decorator that times function execution."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "Done"
result = slow_function() # slow_function took 1.0012 seconds
The *args and **kwargs ensure the decorator works with any function signature.
Preserving Function Metadata
When you create a decorator, the wrapped function loses its original metadata (name, docstring, etc.). Use functools.wraps to preserve it:
from functools import wraps
def timer(func):
"""Decorator that times function execution."""
@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")
return result
return wrapper
@timer
def calculate_sum(numbers):
"""Calculate the sum of numbers."""
return sum(numbers)
print(calculate_sum.__name__) # calculate_sum (not 'wrapper')
print(calculate_sum.__doc__) # Calculate the sum of numbers.
Decorators with Arguments
Sometimes you need decorators that accept arguments. This requires an extra layer of nesting:
def repeat(times):
"""Decorator that repeats a function call."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
Here's another example with a retry decorator:
import time
from functools import wraps
def retry(max_attempts=3, delay=1):
"""Retry a function if it raises an exception."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=1)
def unreliable_function():
import random
if random.random() < 0.7:
raise ValueError("Random failure!")
return "Success!"
Multiple Decorators
You can stack multiple decorators on a single function. They're applied from bottom to top:
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}!"
print(greet("Alice")) # <b><i>Hello, Alice!</i></b>
Built-in Decorators
Python provides several useful built-in decorators:
@property
The @property decorator lets you define methods that are accessed like attributes:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
@property
def area(self):
"""Calculate the area."""
return 3.14159 * self._radius ** 2
circle = Circle(5)
print(circle.radius) # 5 (accessed like attribute)
print(circle.area) # 78.53975
circle.radius = 10
print(circle.area) # 314.159
@staticmethod and @classmethod
These decorators define methods that don't require an instance:
class MathUtils:
PI = 3.14159
@staticmethod
def add(a, b):
"""Static method - no access to class or instance."""
return a + b
@classmethod
def get_pi(cls):
"""Class method - has access to class but not instance."""
return cls.PI
# Can be called on class or instance
print(MathUtils.add(3, 4)) # 7
print(MathUtils.get_pi()) # 3.14159
utils = MathUtils()
print(utils.add(5, 6)) # 11
print(utils.get_pi()) # 3.14159
Practical Decorator Examples
Here are real-world decorator patterns:
# Example 1: Logging decorator
from functools import wraps
import logging
logging.basicConfig(level=logging.INFO)
def log_function_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logging.info(f"{func.__name__} returned {result}")
return result
return wrapper
@log_function_call
def add(a, b):
return a + b
result = add(3, 4) # Logs function call and return value
# Example 2: Validation decorator
def validate_positive(func):
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError("All arguments must be positive")
return func(*args, **kwargs)
return wrapper
@validate_positive
def divide(a, b):
return a / b
# Example 3: Caching decorator (simple version)
from functools import wraps
def cache(func):
cache_dict = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache_dict:
cache_dict[key] = func(*args, **kwargs)
return cache_dict[key]
return wrapper
@cache
def expensive_calculation(n):
print(f"Calculating for {n}...")
return sum(i ** 2 for i in range(n))
print(expensive_calculation(1000)) # Calculates
print(expensive_calculation(1000)) # Uses cache (no calculation)
Try It Yourself
Practice creating decorators:
-
Timing Decorator: Create a decorator that times function execution and only prints if it takes longer than a specified threshold.
-
Rate Limiting: Create a decorator that limits how many times a function can be called per minute.
-
Type Checking: Create a decorator that validates function arguments are of the correct type.
-
Memoization: Improve the caching decorator to handle mutable arguments properly.
-
Authorization: Create a decorator that checks if a user has permission to call a function.
Summary
Decorators are a powerful Python feature that let you modify function behavior without changing their code. You can create simple decorators, decorators with arguments, and stack multiple decorators. Built-in decorators like @property, @staticmethod, and @classmethod are essential for object-oriented programming.
Decorators appear throughout Python's ecosystem and are used for logging, timing, caching, validation, and more. Understanding them will help you write more maintainable, reusable code and better understand how Python frameworks work.
What's Next?
In the next lesson, we'll explore generators and iterators. You'll learn how to create memory-efficient iterators using the yield keyword, understand when generators are better than lists, and create custom iterable objects. These concepts are essential for working with large datasets efficiently.