4
1

More than 3 years have passed since last update.

Pythonのループ処理中でlambdaを使ったときに間違いやすいポイント

Last updated at Posted at 2020-03-14

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)
4
1
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1