Pythonで「並列で500個のファイルをダウンロードする」ために以下のようなコードを書きました。
from time import sleep
from concurrent.futures import ThreadPoolExecutor
def func(i):
# 実際にはファイルのダウンロード処理
sleep(1)
print(f"{i}\n", flush=True, end="")
with ThreadPoolExecutor(max_workers=3) as executor:
for i in range(500):
# 0~499番のファイルを並列でダウンロードする
executor.submit(
lambda: func(i)
)
これを実行すると、以下のように「同じ数字で何度も処理が実行されてしまう」ような挙動になってしまいました。
499
499
499
499
499
499
144
145
146
1
0
2
(以下略)
どうやら、「実際にfuncが実行される時点でi
の値を読み込んでおり、そのためループ最後の時点の i
の値が多数読み込まれてしまう」ようです。
こちらは以下のように functools.partial
を使うことで、当初期待した通りの「0~499までのすべての i
の値で処理を実行する」ように実行されます。
from time import sleep
from functools import partial
from concurrent.futures import ThreadPoolExecutor
def func(i):
sleep(1)
print(f"{i}\n", flush=True, end="")
with ThreadPoolExecutor(max_workers=3) as executor:
for i in range(500):
executor.submit(
partial(func, i)
)
出力はこのようになります。
0
2
1
3
5
4
6
7
8
9
10
11
12
13
14
(以下略)
また、こちらのブログで知ったのですが、Pythonのデフォルト引数で束縛する方法もあるようです。私は partial
を使ったほうがプログラムの意図が伝わりやすいと思うのですが、たしかに面白いですね。
from time import sleep
from concurrent.futures import ThreadPoolExecutor
def func(i):
sleep(1)
print(f"{i}\n", flush=True, end="")
with ThreadPoolExecutor(max_workers=3) as executor:
for i in range(500):
executor.submit(
lambda x=i: func(x)
)
追記
友人から「submit
は実行する関数の引数を受け取れるけどそれじゃダメなの?」ってツッコミを貰ったのですが、全くそのとおりだと思います。実際に使うときのコードはこちらにしましょう。
with ThreadPoolExecutor(max_workers=3) as executor:
for i in range(500):
executor.submit(func, i)