Introduction
Decorators are a powerful feature in Python that allow developers to modify the behavior of functions or methods without changing their code. They provide a way to wrap functions, enabling the injection of additional logic before and/or after the execution of the target function.
A decorator returns another function, which acts as a wrapper around the original function.
It’s easy to confuse decorators with middleware, but they serve different purposes. Middleware operates at a global level, modifying requests and responses during an application's processing cycle. In contrast, decorators work at the function level, affecting only the function they are applied to.
The intent of this article is to discuss decorators and explore how using async changes their behavior.
Synchronous Decorator
Let’s start with a basic synchronous decorator that prints a statement before and after the function execution.
from functools import wraps
class User:
def __init__(self, name, age):
self.name = name
self.age = age
# our decorator
def sample_decorator(func):
@wraps(func)
# decorator wrapper function, which will print statement before and after the execution of func
def my_wrapper():
print("I'm going to execute this function now")
result = func()
print("I'm done executing this function")
return result
return my_wrapper
# calling decorators
@sample_decorator
def sample() -> User:
print("I'm inside the function now")
new_user = User("Vipul", 34)
print(f"I have created a user obj, will return it now: {new_user.name}")
return new_user
new_user = sample()
print(f"Name: {new_user.name}. Age: {new_user.age}")
The code is quite simple. A function sample() returns an object of type User. The decorator "sample_decorator" prints a statement before and after the execution of this function.
Output
I'm going to execute this function now
I'm inside the function now
I have created a user obj, will return it now: Vipul
I'm done executing this function
Name: Vipul. Age: 34
The code above is straightforward. The function sample() returns a User object, and the decorator sample_decorator ensures that a statement is printed before and after its execution.
Handling Asynchronous Functions
Everything above was synchronous, so it worked without issues. But what if either the decorator or the decorated function was asynchronous?
Consider a scenario where another function, sample2(), wants to use the same decorator. One possible approach could be to create a separate async decorator, but that would lead to redundant code when scaled across a larger codebase.
To handle both sync and async functions, we modify the wrapper function to be asynchronous. Before executing func(), we check if it is a coroutine (i.e., an async function). If it is, we await it; otherwise, we execute it normally.
Note: An async function’s return type is a coroutine until it is awaited (this topic requires deeper discussion beyond this explanation).
Updated Code
import asyncio
from functools import wraps
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def async_sample_decorator(func):
@wraps(func)
async def async_my_wrapper():
print("I'm going to execute this function now")
if asyncio.iscoroutinefunction(func):
result = await func()
else:
result = func()
print("I'm done executing this function")
return result
return async_my_wrapper
@async_sample_decorator
async def sample() -> User:
print("I'm inside the function now")
new_user = User("Vipul", 34)
print(f"I have created a user obj, will return it now: {new_user.name}")
return new_user
@async_sample_decorator
def sample2() -> User:
print("I'm inside the function now")
new_user = User("Malhotra", 34)
print(f"I have created a user obj, will return it now: {new_user.name}")
return new_user
async def main():
new_user = await sample()
print(f"Name: {new_user.name}. Age: {new_user.age}")
new_user2 = await sample2()
print(f"Name: {new_user2.name}. Age: {new_user2.age}")
asyncio.run(main())
Output
I'm going to execute this function now
I'm inside the function now
I have created a user obj, will return it now: Vipul
I'm done executing this function
Name: Vipul. Age: 34
I'm going to execute this function now
I'm inside the function now
I have created a user obj, will return it now: Malhotra
I'm done executing this function
Name: Malhotra. Age: 34
As seen in the output, the same decorator and wrapper function successfully handle both synchronous and asynchronous functions.
The code is part of my ongoing git repository where I push small logic related to Python.
Github Link for reference: https://github.com/vipulm124/python-concepts.git