経緯
- 2重ループを廃止して可読性を上げたい
※ループがネストしていると単体テストを書きにくい、という問題もある - しかし、
itertools.product()
は遅いという情報を入手した
「2.」の情報元は以下の記事
リファクタリングしたせいで処理が遅くなったら困るので、どのくらい遅くなるのか、自分で確かめることにした。
環境
測定は、お手軽に試せる Google Colaboratory 上で行いました。
!python --version
Python 3.7.13
調査 part.1 (2重ループ)
比較対象
- 2重forループ
-
itertools.product
に対するループ
ソースコード
1. ループ対象
まず、ループ対象になる配列を生成しておく。
これをループさせる。(今回はrangeとした)
ran1 = range(1, 10000)
ran2 = range(1, 500)
実際の運用で想定されるのよりも、ちょっと多めのloop数にしておいた。
2. 2重ループ
%%timeit -r 5 -n 20
cnt = 0
for i in ran1:
for j in ran2:
cnt += i * j
3. itertools.product()
のループ
%%timeit -r 5 -n 20
import itertools
cnt = 0
iterator = itertools.product(ran1, ran2)
for i, j in iterator:
cnt += i * j
結果
ループ種別 | 測定結果 | 測定結果詳細 |
---|---|---|
2重ループ | 505 ms | 20 loops, best of 5: 505 ms per loop |
itertools.product()ループ | 556 ms | 20 loops, best of 5: 556 ms per loop |
んーーーー10%の遅延??
大差ないじゃんかーーーもー(´^ω^`)
stackoverflowにビビらされただけでした。
調査 part.2 (5重ループ)
...で、終わったら記事として物足りないので、多重ループさせた場合の比較もしてみることにした。
stackoverflowは2重じゃなくて多重ループを取り上げてましたしね。
別に目の前の仕事では必要なかったんですけど、、、いつか使うかもしれないし。。。いや、使いたくないけど(。ŏ﹏ŏ)
比較対象
- 5重forループ
-
itertools.product
に対するループ
ソースコード
1. ループ対象
ran1 = range(1, 10)
ran2 = range(1, 5)
ran3 = range(1, 21)
ran4 = range(1, 18)
ran5 = range(1, 6)
ループ数は適当
2. 5重ループ
%%timeit -r 5 -n 20
cnt = 0
for i in ran1:
for j in ran2:
for k in ran3:
for l in ran4:
for m in ran5:
cnt += i + j + k + l + m
3. itertools.product()
のループ
%%timeit -r 5 -n 20
import itertools
cnt = 0
iterator = itertools.product(ran1, ran2, ran3, ran4, ran5)
for i, j, k, l, m in iterator:
cnt += i + j + k + l + m
結果
ループ種別 | 測定結果 | 測定結果詳細 |
---|---|---|
2重ループ | 52.5 ms | 20 loops, best of 5: 52.5 ms per loop |
itertools.product()ループ | 63.7 ms | 20 loops, best of 5: 63.7 ms per loop |
ループのネスト数が増えたら、確かに遅延割合も大きくなった。
2重のネスト程度なら10%遅延で済むけど、5重ネストだと20%程度遅延することもあるみたい。
考察
itertools.productの導入に当たっては、ループ数とループのネスト数に注意する必要があるのではないかと思う。
たとえばループ内の処理がそもそも重い処理(ファイル出力とか画像読み込みとか?)で、ループ数もネスト数も少ない場合は、この遅延はそんなに気にならないはず。
理由としては、重い処理自体が更に遅くなるわけではないからである。
あくまでも、1ループごとに余計なオフセットがうっすら上乗せされるだけなので、loopのネストをitertools.productに置き換えたところで、ループ数もネスト数も少なければ、誤差程度の遅延が発生するだけだろう。
たとえば、2時間かかってた処理が2時間3分になるとかその程度だと思われる。
逆に、ループ内の処理ではなくて、ループ数やネスト数が膨大なために処理時間がかかっているような場合は、今回の調査結果のように10%や20%の遅延が発生するので、itertools.productに置き換えるべきかどうかは落ち着いて考えた方が良いかもしれない。
また、5重以上のループの場合は当然20%以上の遅延が発生すると思われるので、注意が必要だろう。
stackoverflowに掲載されていた8重ループの例では、60%ほどの遅延が発生していた。
補足
前記stackoverflowの回答のうち、この人の回答
に従えば遅延を防げるのかもしれないけど、今回の当初の目的は可読性を上げることだったので、なかなか理解しにくいこの実装は候補から除外した。