Decorators are a powerful and flexible feature in Python that allows you to modify the behavior of a function or a class.
In simple terms, a decorator is a function that takes another function and extends or alters its behavior without modifying it directly.
Decorators are often used to add functionality like logging, access control, performance measurement, or memoization to existing functions.
In this tutorial, you'll learn:
What are decorators?
How to create and use function decorators
Using multiple decorators
Decorating functions with arguments
Class-based decorators
Practical examples of decorators
1. What Are Decorators?
A decorator is a function that takes another function as input and returns a new function that extends the behavior of the original one. This is done without modifying the original function's code.
Here is a simple template for a decorator:
def my_decorator(func): def wrapper(): # Code to run before the function call print("Before the function call") # Call the original function func() # Code to run after the function call print("After the function call") return wrapper
When you apply the decorator to a function, it wraps that function with additional functionality.
2. How to Create and Use Function Decorators
Let’s start by creating a simple decorator and applying it to a function.
Example 1: A Simple Decorator
def my_decorator(func): def wrapper(): print("Something before the function") func() print("Something after the function") return wrapper # Applying the decorator to a function @my_decorator def say_hello(): print("Hello!") # Call the decorated function say_hello()
Output:
Something before the function Hello! Something after the function
In this example, say_hello() is decorated with my_decorator, which means that the original function is wrapped with extra functionality: printing something before and after the function call.
The @my_decorator syntax is a shortcut for:
say_hello = my_decorator(say_hello)
3. Using Multiple Decorators
You can apply more than one decorator to a function by stacking them. Python applies decorators in a bottom-up order (i.e., the one closest to the function runs first).
Example 2: Stacking Multiple Decorators
def decorator_one(func): def wrapper(): print("Decorator One: Before function") func() print("Decorator One: After function") return wrapper def decorator_two(func): def wrapper(): print("Decorator Two: Before function") func() print("Decorator Two: After function") return wrapper @decorator_one @decorator_two def greet(): print("Hello!") greet()
Output:
Decorator One: Before function Decorator Two: Before function Hello! Decorator Two: After function Decorator One: After function
In this example, decorator_two runs first because it's closest to the function, and decorator_one runs around it.
4. Decorating Functions with Arguments
When you need to decorate functions that accept arguments, you must modify the decorator to accept *args and **kwargs. This ensures that your decorator can handle any number and type of arguments.
Example 3: A Decorator for Functions with Arguments
def my_decorator(func): def wrapper(*args, **kwargs): print("Before the function") result = func(*args, **kwargs) print("After the function") return result return wrapper @my_decorator def add(a, b): return a + b # Call the decorated function result = add(5, 3) print(f"Result: {result}")
Output:
Before the function After the function Result: 8
The decorator works with any number of positional (*args) or keyword arguments (**kwargs) that the original function accepts.
5. Class-Based Decorators
While most decorators are functions, you can also create decorators using classes. Class-based decorators are useful when you want more control or need to maintain state.
Example 4: A Class-Based Decorator
class MyDecorator: def __init__(self, func): self.func = func def __call__(self, *args, **kwargs): print("Class-based: Before the function") result = self.func(*args, **kwargs) print("Class-based: After the function") return result @MyDecorator def multiply(a, b): return a * b result = multiply(5, 4) print(f"Result: {result}")
Output:
Class-based: Before the function Class-based: After the function Result: 20
In this example, the class MyDecorator acts as a decorator by implementing the __call__() method. This allows the class to behave like a function and be used as a decorator.
6. Practical Examples of Decorators
Example 1: Logging Decorator
A logging decorator can be used to log when a function is called and its return value.
def logger(func): def wrapper(*args, **kwargs): print(f"Calling function: {func.__name__}") result = func(*args, **kwargs) print(f"Function {func.__name__} returned: {result}") return result return wrapper @logger def subtract(a, b): return a - b result = subtract(10, 3)
Output:
Calling function: subtract Function subtract returned: 7
Example 2: Timing Function Execution
You can use a decorator to measure how long a function takes to execute, which is useful for performance monitoring.
import time def timer(func): def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) end_time = time.time() print(f"Function {func.__name__} took {end_time - start_time:.4f} seconds") return result return wrapper @timer def slow_function(): time.sleep(2) print("Function finished") slow_function()
Output:
Function finished Function slow_function took 2.0001 seconds
Example 3: Access Control Decorator
An access control decorator can be used to restrict access to a function based on a condition (e.g., user authentication).
def requires_authentication(func): def wrapper(user_authenticated, *args, **kwargs): if user_authenticated: return func(*args, **kwargs) else: print("Access Denied: User not authenticated") return wrapper @requires_authentication def view_dashboard(): print("Welcome to the dashboard!") # Test the decorator view_dashboard(True) # User is authenticated view_dashboard(False) # User is not authenticated
Output:
Welcome to the dashboard! Access Denied: User not authenticated
Example 4: Caching Results (Memoization)
A decorator can cache the result of expensive function calls and return the cached result when the same inputs are provided again.
def cache(func): cached_results = {} def wrapper(*args): if args in cached_results: print("Returning cached result") return cached_results[args] result = func(*args) cached_results[args] = result return result return wrapper @cache def fibonacci(n): if n in [0, 1]: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(10)) print(fibonacci(10)) # Cached result
Output:
55 Returning cached result 55
Conclusion
Decorators are a versatile and powerful feature in Python that allows you to modify the behavior of functions and classes without changing their actual implementation. Here's a summary of what you’ve learned:
How to create and apply function decorators.
How to apply multiple decorators to a single function.
How to decorate functions that accept arguments using *args and **kwargs.
How to create class-based decorators.
Practical use cases like logging, timing, access control, and caching.
With these concepts, you're ready to start using decorators to cleanly and efficiently extend your Python functions!