3

Here is an async function generator by iterating a for loop. I expected this closure to respect the names from the outer scope.

import asyncio

coroutines = []
for param in (1, 3, 5, 7, 9):

    async def coro():
        print(param ** 2)
        # await some_other_function()
    
    coroutines.append(coro)


# The below code is not async, I made a poor mistake.
# Michael Szczesny's answer fixes this.
for coro in coroutines:
    asyncio.run(coro())

While I was expecting the results to be 1, 9, 25, 49, 81 after running, here is the actual output:

81
81
81
81
81

Unexpectedly here, the name param has taken the same value each time.

Can you explain why this happens and how I can achieve the task of creating lots of async functions in a for loop with names binding correctly?

UpTheIrons
  • 73
  • 1
  • 6

2 Answers2

2

The included code does not run asynchronously or uses coro as a closure. Functions are evaluated at runtime in python. An asynchronous solution would look like this

import asyncio

def create_task(param):
    async def coro():
        await asyncio.sleep(1) # make async execution noticeable
        print(param ** 2)
    return coro                # return closure capturing param

async def main():
    await asyncio.gather(*[create_task(param)() for param in (1,3,5,7,9)])

asyncio.run(main())

Output after 1 second

1
9
25
49
81
Michael Szczesny
  • 4,911
  • 5
  • 15
  • 32
1

Only once the coro function is run, will it evaluate the line:

print(param ** 2)

At that time, it will check the value of param and evaluate it. So what is the value of param then?

Looking at the code, we can see that the param value is set during iteration:

for param in (1, 3, 5, 7, 9):
    ...

# For loop is over, param is now equal to 9
# since it was the last value it iterated on

Then only afterwards is the coro function run:

for coro in coroutines:
    asyncio.run(coro())  # 'param' is still set to 9, meaning that the output will always be 81

You could instead add a parameter to the coro function:

async def coro(coro_param):
    print(coro_param ** 2)

... and pass the parameter to the function when it is called:

for param in (1, 3, 5, 7, 9):
    asyncio.run(coro(param))
Xiddoc
  • 3,369
  • 3
  • 11
  • 37