Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What mechanism makes Python lambdas work without await keyword?

I just noticed something surprising. Consider the following example:

import asyncio

async def wait_n(n):
    asyncio.sleep(n)

async def main(fn):
    print("meh")
    await fn(1)
    print("foo")

loop = asyncio.get_event_loop()
loop.run_until_complete(main(wait_n))

When we run this, we rightfully receive the following warning:

awaitable_lambda.py:5: RuntimeWarning: coroutine 'sleep' was never awaited

asyncio.sleep(n)

This is because in wait_n we called asyncio.sleep(n) without await.

But now consider the second example:

import asyncio

async def main(fn):
    print("meh")
    await fn(1)
    print("foo")

loop = asyncio.get_event_loop()
loop.run_until_complete(main(lambda n: asyncio.sleep(n)))

This time we are using a lambda and surprisingly the code works just fine even though there is no await.

I understand that we can not use await inside a Python lambda expression so this seems like a feature to improve ergonomics but it leads to me some questions:

  1. How exactly does this work? Does this simple "inject" an await before any coroutine function?
  2. Is this documented somewhere (PEP)?
  3. Are there any other implications of this? Can we just safely call coroutinefunctions from lambda expressions and rely on Python to await things for us?
like image 990
Christoph Avatar asked Feb 03 '23 18:02

Christoph


1 Answers

Any asynchronous function returns an awaitable. You don't need to "await the function call" immediately, you just need to await the returned awaitable value eventually. I.e., these two are equivalent:

await asyncio.sleep(1)
awaitable = asyncio.sleep(1)
await awaitable

As such, it should be easy to see that the call fn(1) of the lambda (implicitly) returns an awaitable, and await awaits it. The async def wait_n on the other hand never returns the sleep awaitable and never awaits it itself.

As a corollary example of this, if you have any wrapper around an async function, there's not necessarily a need for that wrapper to be async itself:

def add_1(func):
    def wrapper(a):
        return func(a + 1)  # passes the awaitable return value through

    return wrapper

@add_1
async def foo(a):
    await asyncio.sleep(a)

async def main():
    await foo(1)
like image 77
deceze Avatar answered Feb 06 '23 08:02

deceze