Sarang Babu
Sarang Babu

Reputation: 11

Python coroutine

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

Answers (2)

user4815162342
user4815162342

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

jsbueno
jsbueno

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

Related Questions