Decorators in Python are a powerful feature that allows you to modify the behavior of functions or methods. A decorator is essentially a function that wraps another function, adding extra functionality before or after the original function without modifying its core logic. This promotes code reusability and clean separation of concerns.

### Basics of Decorators

The basic idea of a decorator is to take a function, extend or alter its behavior, and return it.

“`python
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 syntactic sugar for `say_hello = my_decorator(say_hello)`. This wraps the `say_hello` function in `my_decorator`.

### Common Patterns with Decorators

#### 1. Logging

Logging is a common use case for decorators to keep track of function calls and parameters.

“`python
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f”Calling {func.__name__} with {args} and {kwargs}”)
result = func(*args, **kwargs)
print(f”{func.__name__} returned {result}”)
return result
return wrapper

@log_decorator
def add(a, b):
return a + b

add(2, 3)
“`

#### 2. Timing

Decorators can be used to measure the execution time of a function.

“`python
import time

def timing_decorator(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”)
return result
return wrapper

@timing_decorator
def compute_square(n):
return n * n

compute_square(10)
“`

#### 3. Caching

A caching decorator can store results of expensive function calls and return the cached result when the same inputs occur again.

“`python
def cache_decorator(func):
cache = {}
def wrapper(*args):
if args in cache:
return cache[args]
else:
result = func(*args)
cache[args] = result
return result
return wrapper

@cache_decorator
def slow_function(n):
time.sleep(2) # Simulating a slow function
return n * n

print(slow_function(3))
print(slow_function(3)) # This call will be fast
“`

#### 4. Authentication

This can be used to check if a user is authenticated before executing a function.

“`python
def auth_decorator(func):
def wrapper(user, *args, **kwargs):
if not user.get(“authenticated”):
print(f”Authentication failed for {user[‘name’]}”)
return
return func(user, *args, **kwargs)
return wrapper

@auth_decorator
def get_data(user, data_id):
print(f”Fetching data for {data_id}”)

user1 = {“name”: “Alice”, “authenticated”: True}
user2 = {“name”: “Bob”, “authenticated”: False}

get_data(user1, 42)
get_data(user2, 42)
“`

### Using `functools.wraps`

When you create a decorator, it replaces the decorated function with the wrapper function. This can result in losing important metadata, such as the name, docstring, and module of the original function. `functools.wraps` is used to preserve this metadata.

“`python
from functools import wraps

def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(“Pre-processing logic”)
result = func(*args, **kwargs)
print(“Post-processing logic”)
return result
return wrapper

@my_decorator
def my_function():
“This is a docstring for my_function”
print(“Function logic”)

print(my_function.__name__) # Output: my_function
print(my_function.__doc__) # Output: This is a docstring for my_function
“`

In this example, `@wraps(func)` ensures that the `wrapper` function retains the name, docstring, and other attributes of `my_function`.

### Conclusion

Decorators are a crucial part of Python’s functional programming toolkit. They allow for more modular and organized code by promoting code reuse and separation of concerns. Using `functools.wraps`, you can retain important metadata and make your decorated functions behave more naturally.

Scroll to Top