Python - Python Decorators and Function Wrappers
Python decorators are a powerful feature that allows programmers to modify or extend the behavior of functions and methods without changing their actual source code. Decorators are widely used in Python frameworks and libraries for tasks such as logging, authentication, access control, performance monitoring, caching, and input validation.
What is a Decorator?
A decorator is a function that takes another function as an argument, adds some functionality to it, and returns a modified version of that function.
In Python, functions are considered first-class objects, which means they can be:
-
Assigned to variables
-
Passed as arguments to other functions
-
Returned from functions
-
Stored in data structures
This capability makes decorators possible.
Basic Example
def greet():
print("Hello, World!")
def decorator_function(func):
def wrapper():
print("Before the function call")
func()
print("After the function call")
return wrapper
decorated_greet = decorator_function(greet)
decorated_greet()
Output:
Before the function call
Hello, World!
After the function call
In this example:
-
greet()is the original function. -
decorator_function()acts as a decorator. -
wrapper()adds extra functionality before and after the original function execution.
Using the @ Symbol
Python provides a cleaner syntax using the @ symbol.
def decorator_function(func):
def wrapper():
print("Before execution")
func()
print("After execution")
return wrapper
@decorator_function
def greet():
print("Hello")
greet()
The statement:
@decorator_function
is equivalent to:
greet = decorator_function(greet)
This makes code more readable and easier to maintain.
Understanding Function Wrappers
A wrapper function is an inner function that surrounds another function's execution with additional logic.
Structure:
def decorator(func):
def wrapper():
# Additional code
func()
# Additional code
return wrapper
The wrapper function is responsible for:
-
Receiving control
-
Performing extra operations
-
Calling the original function
-
Returning results if needed
Decorators with Function Arguments
The previous examples only work for functions without parameters.
To handle arguments:
def decorator(func):
def wrapper(name):
print("Welcome")
func(name)
print("Goodbye")
return wrapper
@decorator
def greet(name):
print(f"Hello, {name}")
greet("John")
Output:
Welcome
Hello, John
Goodbye
The wrapper accepts the same parameters as the original function.
Using *args and **kwargs
A more flexible approach is to use *args and **kwargs.
def decorator(func):
def wrapper(*args, **kwargs):
print("Function started")
result = func(*args, **kwargs)
print("Function completed")
return result
return wrapper
Example:
@decorator
def add(a, b):
return a + b
print(add(10, 20))
Output:
Function started
Function completed
30
This decorator can work with any function regardless of the number of arguments.
Returning Values from Decorated Functions
Decorators should often preserve return values.
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
@decorator
def square(num):
return num * num
print(square(5))
Output:
25
Without returning the result, the original function's output would be lost.
Practical Example: Logging Decorator
Logging helps track program execution.
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} finished")
return result
return wrapper
@logger
def multiply(a, b):
return a * b
print(multiply(4, 5))
Output:
Calling function: multiply
Function multiply finished
20
This technique is commonly used in large applications.
Timing Execution with Decorators
Decorators can measure execution time.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print("Execution Time:", end - start)
return result
return wrapper
@timer
def calculate():
total = 0
for i in range(1000000):
total += i
calculate()
This is useful for performance analysis.
Decorators with Parameters
Sometimes decorators themselves need arguments.
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for i in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def greet():
print("Hello")
greet()
Output:
Hello
Hello
Hello
Here:
-
repeat(3)creates a decorator. -
The decorator modifies the behavior of the target function.
Preserving Function Metadata
When a function is decorated, information such as its name and documentation may be lost.
Example:
def decorator(func):
def wrapper():
func()
return wrapper
@decorator
def greet():
"""Greeting Function"""
print("Hello")
print(greet.__name__)
Output:
wrapper
The original function name is replaced by wrapper.
To preserve metadata, use functools.wraps.
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
Now the original function details remain intact.
Chaining Multiple Decorators
More than one decorator can be applied to a function.
def decorator1(func):
def wrapper():
print("Decorator 1")
func()
return wrapper
def decorator2(func):
def wrapper():
print("Decorator 2")
func()
return wrapper
@decorator1
@decorator2
def greet():
print("Hello")
greet()
Output:
Decorator 1
Decorator 2
Hello
Decorators are applied from the bottom upward.
Real-World Applications of Decorators
Authentication
def login_required(func):
def wrapper():
authenticated = True
if authenticated:
return func()
else:
print("Access Denied")
return wrapper
Input Validation
def validate(func):
def wrapper(age):
if age < 0:
print("Invalid age")
return
return func(age)
return wrapper
Caching
from functools import lru_cache
@lru_cache(maxsize=100)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Caching improves performance by storing previously computed results.
Advantages of Decorators
-
Improve code reusability.
-
Reduce duplication.
-
Separate business logic from auxiliary tasks.
-
Enhance readability.
-
Simplify maintenance.
-
Allow dynamic behavior modification.
Limitations of Decorators
-
Can make debugging more complex.
-
Excessive nesting may reduce readability.
-
Improper implementation may hide function metadata.
-
Multiple decorators can make execution flow difficult to follow.
Summary
Decorators are a powerful Python feature that enables developers to add functionality to existing functions without modifying their code. They work by wrapping functions inside other functions and are extensively used for logging, authentication, caching, performance monitoring, validation, and many framework-level operations. Understanding decorators is essential for advanced Python development because they promote reusable, modular, and maintainable code while keeping the core business logic clean and organized.