Why async function binds name incorrectly from the outer scope?
Question:
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?
Edit: Here is what my program looks like in A LOT MORE detail even though the actual problem is the above. Here you’ll see that why coro
function shouldn’t take param
as a parameter in my case.
import asyncio
from dataclasses import dataclass
from typing import Any
# Using dataclass to make IDE give auto-completion suggestions.
@dataclass
class Params:
first: Any
second: Any
third: Any
fourth: Any
fifth: Any
class MyClass:
def __init__(self):
coroutines = []
for param in (1, 3, 5, 7, 9):
async def coro():
await asyncio.sleep(1)
print(param ** 2)
coroutines.append(coro)
self.run = Params(*coroutines)
async def main():
obj = MyClass()
await asyncio.gather(
obj.run.first(), # each of them should be uniquely defined...
obj.run.second(), # ...by `param` in the `for` loop.
obj.run.third(),
obj.run.fourth(),
obj.run.fifth()
)
asyncio.run(main())
Output:
81
81
81
81
81
Answers:
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))
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
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?
Edit: Here is what my program looks like in A LOT MORE detail even though the actual problem is the above. Here you’ll see that why coro
function shouldn’t take param
as a parameter in my case.
import asyncio
from dataclasses import dataclass
from typing import Any
# Using dataclass to make IDE give auto-completion suggestions.
@dataclass
class Params:
first: Any
second: Any
third: Any
fourth: Any
fifth: Any
class MyClass:
def __init__(self):
coroutines = []
for param in (1, 3, 5, 7, 9):
async def coro():
await asyncio.sleep(1)
print(param ** 2)
coroutines.append(coro)
self.run = Params(*coroutines)
async def main():
obj = MyClass()
await asyncio.gather(
obj.run.first(), # each of them should be uniquely defined...
obj.run.second(), # ...by `param` in the `for` loop.
obj.run.third(),
obj.run.fourth(),
obj.run.fifth()
)
asyncio.run(main())
Output:
81
81
81
81
81
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))
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