Decorators in Python are a way to modify or enhance functions or classes without directly changing their source code.
They use the concept of closures and higher-order functions to wrap the original function or class, adding functionality before or after the wrapped code executes.
It is similar to Router guards with Vue Router or custom attributes or middleware in C#.
Let’s break down step by step what you can code on and how it works .
Basic Example
Let’s start with the basics. You define a Python decorator as follows:
1
2
3
4
5
6
7
|
def my_decorator(original_function):
def wrapper_function(*args, **kwargs):
# Code to execute before the `original_function`
result = original_function(*args, **kwargs)
# Code to execute after the `original_function`
return result
return wrapper_function
|
Now, let’s look at specific use cases.
This is the simplest form of a decorator. It doesn’t have any arguments other than the function it’s decorating. In between, it prints out In decorator before calling {function name}
and Function {function name} called. Completing decorator logic...
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
def log_function_call(func):
def wrapper(*args, **kwargs):
print(f"In decorator before calling <{func.__name__}>")
result = func(*args, **kwargs)
print(f"Function <{func.__name__}> called. Completing decorator logic...")
return result
return wrapper
@log_function_call
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Ouputs:
# "In decorator before calling <greet>
# "Function <greet> called. Completing decorator logic...
|
In this example, the log_function_call
decorator adds logging before and after the function call without needing any input from the caller.
When you need to pass arguments to the decorator itself, you need to add another layer of functions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
def repeat(times):
def decorator(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("Bob")
|
In this case, the repeat
decorator has an argument times
that determines how many times the decorated function greet
should be called.
Here’s how it works:
repeat(times)
is called with the argument, returning the decorator
function.
- The
decorator
function then wraps the original function (greet
in this case).
- When
greet
is called, it actually calls the wrapper
function, which executes the original greet
function times
number of times.
The Order Matters
You can combine multiple decorators:
1
2
3
4
|
@decorator1
@decorator2(arg)
def my_function():
pass
|
This is equivalent to:
1
|
my_function = decorator1(decorator2(arg)(my_function))
|
Ordering your decorators is crucial and will depend on your business logic. Take time to identify the proper order.
Practical Example
Let’s say we have this endpoint intercepting an incoming call webhook:
1
2
3
4
5
6
7
8
9
10
11
|
from twilio.twiml.voice_response import VoiceResponse
from app.modules.call import call
from app.commons.decorators import need_xml_output, log_headers, validate_twilio_request
@call.route('/incoming', methods=['POST'])
@validate_twilio_request
@log_headers
@need_xml_output()
def redirecting_call() -> VoiceResponse:
# find whom to redirect the call to...
|
The decorators we want to look at are log_headers
and validate_twilio_request
.
They look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
def log_headers(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
current_app.logger.info(f"Request Headers: {dict(request.headers)}")
return f(*args, **kwargs)
except Exception as e:
current_app.logger.error(f"Exception in decorator <log_headers>: {e}")
return decorated_function
def validate_twilio_request(f):
print("Decorator validate_twilio_request called")
@wraps(f)
def decorated_function(*args, **kwargs):
try:
# List all the data that make a signature
current_app.logger.debug("Inside decorated function")
auth_token = current_app.config['TWILIO_AUTH_TOKEN']
url = _get_url()
post_data = request.form
# The X-Twilio-Signature header
twilio_signature = request.headers.get('X-Twilio-Signature', '')
current_app.logger.debug(f"AUTH_TOKEN={auth_token}")
current_app.logger.debug(f"url={url}")
current_app.logger.debug(f"post_data={post_data}")
current_app.logger.debug(f"twilio_signature={twilio_signature}")
# Create a RequestValidator object
validator = RequestValidator(auth_token)
# Validate the request
if not validator.validate(url, post_data, twilio_signature):
# If the request is not valid, return a 403 Forbidden error
abort(403)
# If the request is valid, call the decorated function
return f(*args, **kwargs)
except Exception as e:
current_app.logger.error(f"Exception in decorator <validate_twilio_request>: {e}")
abort(500)
return decorated_function
|
Now, a problem may arise with the log_headers
decorator that fails to execute and trace headers. Why?
In Python, decorators are applied from bottom to top. In our use case, we’d have an equivalent to this:
1
2
|
# ORDER OF EXECUTION
result = need_xml_output(log_headers(validate_twilio_request(call.route(args)))(redirecting_call))
|
The decorator validate_twilio_request
fails if the signature in the X-Twilio-Signature
header is incorrect and therefore the log_headers
won’t execute at all because of the raised error:
1
2
|
if not validator.validate(url, post_data, twilio_signature):
abort(403)
|
To debug the decorator validate_twilio_request
failure, it’s impractical not to know what the headers were.
The fix is simple: place log_headers
first and the log file will contain all the headers received on the request.
Credit: Photo by Nataliya Vaitkevich.