Decoration patterns
So this post touches on some Python concepts in async/await territory. I will not cover event loops nor how to interact with them, but something I uncovered/unearthed in a quest to make something work that was synchronous only. I will use the word async
which itself is a shortening of the word asynchronous.
First things first, the async structure in essence means a form of cooperative multi-threading. This is in contrast with preemptive multi-threading. The latter means there will be scheduled allotted CPU cycles where for example a function needs 3 cycles, but gets 2 at a time over the course of all work that needs to be done can actually take 6 cycles.
In cooperative model the function gets to take as many cycles with regards to the CPU scheduler though and then hand back control after 3 and hopefully that means in this case the cycles are used in a more direct and efficient way making the program smoother and more efficient.
As a side note, async should only be used as you expect to be I/O bound and also it should be used completely throughout the application and third party libraries.
So this also means it will be running in the background whatever this function is you wanted to make async, and therefore the keyword await
was introduced stating you should await the function for when it is done.
Getting started
First something simple.
def f():
pass
A simple function definition. If we wanted an async function we would do the following:
async def a():
pass
So far only the only difference, other than the name, is the keyword async. So what happens when you print these defined symbols.
print(f) # <function f at 0x7fa7b50af1f0>
print(a) # <function a at 0x7fa7b4fb9ca0>
So nothing apparently. They are both just function definitions. What happens when you print the result of executing the functions?
print(f()) # None
print(a()) # <coroutine object a at 0x7fa7b5037940>
You will also get a RuntimeWarning of a coroutine not being awaited. We leave that for what it is right now. So interestingly we see they are both functions before and after the execution the one has a result, the other a coroutine object. So we want to see if there is a way to determine if the function is a coroutine aforehand.
Let us write something that does that:
import inspect
print(inspect.iscoroutine(a)) # False
print(inspect.isawaitable(a)) # False
print(inspect.iscoroutine(a())) # True
print(inspect.isawaitable(a())) # True
Hang on, they both state False
when operating on the definition. However our RuntimeWarning said we needed to await
. When we execute the function we do get the information, but that is still after the fact.
We first have to execute a function in order to find out we need to await
. There might be some people out there right now that go well you used a wrong method. There will be two solutions at the end.
Decorators
In Python there exists the following syntactic sugar:
def decorator(func):
def wrapper():
func()
return wrapper
@decorator
async def a():
pass
a()
This is the same as doing a = decorator(a)
and then calling a()
will actually execute the wrapper, as a
will be now equal to wrapper
.
First problem we run into in this example is we gave an async function to a non-async function. You can only await
in async functions. That is easily solved:
def decorator(func):
async def wrapper():
await func()
return wrapper
Now the wrapper is also async and we await the function inside. However this decorator might be used for async and non-async functions alike. We still need to determine accurately whether or not a function is async aforehand.
Internals
Looking at the internals of Python there exists the __code__
property on functions. Inside that property there is a co_flags
property. That will actually hold a bitmap value of what flags the function itself holds. You can get at this information in the following way:
from dis import pretty_flags
def f():
pass
async def a():
pass
print(pretty_flags(f.__code__.co_flags)) # OPTIMIZED, NEWLOCALS, NOFREE
print(pretty_flags(a.__code__.co_flags)) # OPTIMIZED, NEWLOCALS, NOFREE, COROUTINE
Aha, we see now that we can determine if a function is a coroutine or not. This means we can make our decorator correctly now:
from dis import pretty_flags
def decorator(func):
def wrapper():
func()
async def async_wrapper():
await func()
if "COROUTINE" in pretty_flags(func.__code__.co_flags):
return async_wrapper
return wrapper
Final Solution
I already mentioned there are two solution. I first wanted to get this internal solution out the way as that is the one I used first. Then I ran into something that made more sense. So the before mentioned decorator can also be written thusly:
import inspect
def decorator(func):
def wrapper():
func()
async def async_wrapper():
await func()
if inspect.iscoroutinefunction(func):
return async_wrapper
return wrapper
The importance of using the correct method is abundantly clear in this case. Our definition is a coroutine function, not yet a coroutine and therefor you also cannot await a definition. Only an instance of the executed async function.
Hope this helps out a bit in the future of your async python adventure.
As a final final example the decorator should probably look like this:
import inspect
def decorator(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
async def async_wrapper(*args, **kwargs):
await func(*args, **kwargs)
if inspect.iscoroutinefunction(func):
return async_wrapper
return wrapper
In order to propagate any and all arguments given to the function you are decorating.