Reputation: 11
I have a non-async function and an async-decorator function. Is there any way to decorate the non-async function with an async decorator ?
async def dec():
# decorator body
@dec
def my_func():
# function body
Upvotes: 0
Views: 889
Reputation: 154886
TL;DR Decorators literally defined as async def
don't work. It is perfectly possible to produce async functions from decorators, and jsbueno's answer covers that, but you have to use a regular def
for the decorator itself. The rest of this answer examines what happens when you do try to use async def
to define a decorator.
What you describe is syntactically valid, but doesn't make sense when run. Consider that a decorator such as:
@dec
def my_func():
# ...
...is syntactic sugar for:
def my_func():
# ...
my_func = dec(my_func)
Obviously, dec
must accept a function argument, as all decorators do. But that's not enough: since dec
is an async function, calling it produces a coroutine object. This object can be awaited, but not called. In other words, neither of these will work:
foo = my_func() # TypeError: 'coroutine' object is not callable
foo = await my_func() # TypeError: 'coroutine' object is not callable
What would work is await my_func
- but that would just execute the decorator. Normally decorators are executed at top-level and are expected to return a function that will be called instead of the decorated function. This decorator can't work like that, and has other problems to boot.
Another issue is that await my_func
would work only once, because the await
would exhaust the coroutine object. Normally this isn't an issue because you await
a freshly created coroutine object, as in await bla()
. If you tried x = bla(); await x; await x
, the second await
would also fail.
In conclusion, in current Python async def
cannot be used to define a decorator.
Upvotes: 1
Reputation: 110261
Since in decorators, you define the wrapper function inside its body, it can work this way:
def makeasync(func):
async def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
So, the decorated function would be seem as an async function,and could be awaited. However, if the original func blocks in execution, be it a CPU block, or I/O block, it will stall the async loop, and spoil any advantages from having async code in the first place.
Much more useful would be a mechanism that will run your original function in a separate thread, or even a separate process (for CPU bound functions) - and, guess what, Python's asyncio does have such an utility - the run_in_executor
loop method allows you to spec a function call to a non-async function, and await its result:
https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor
If you want that as a decorator, it is almost trivial:
def makeasync(func):
async def wrapper(*args, **kwargs):
loop = asyncio.get_running_loop()
return await loop.run_in_executor (None, func, *args, **kwargs)
return wrapper
(by default, asyncio instantiates a concurrent.future.ThreadPoolExecutor - which is good if your function is I/O bound - you can have a separate executor to go with the decorator if you don't want the default settings)
Upvotes: 1