括弧が逆ゥ!
はてブ見てたらこういうの見つけた:
Python簡単実験:内包で何倍高速になるか - TIM Labs
「なんで対話型インタプリタ使ってるんや・・・・・?」という疑問は置いといて、こんなに違うのかとちょっと信じ切れなかったので、自分でも書きました。
まずは4つ
作ったのは4つで、
- 単純に for で回す
- 内包表記使う
- range をそのまま list に投げる
- ジェネレータを list に投げる
です。
# coding=utf-8 import time def time_calc(func, *arg, **kwds): start = time.time() func(*arg, **kwds) end = time.time() return end - start def calc(func, n, *arg, **kwds): times = [time_calc(func, *arg, **kwds) for _ in range(n)] return sum(times) / n def simply_for(n): a = list() for i in range(n): a.append(i) return a def comprehension(n): a = [x for x in range(n)] return a def range_case(n): a = list(range(n)) return a def range_(n): for i in range(n): yield i def generator_case(n): a = list(range_(n)) return a if __name__ == '__main__': n = 10**7 for func in (simply_for, comprehension, range_case, generator_case): print("{}: {}sec.".format(func.__name__, calc(func, 5, n)))
おかゆのパソコンはうんちなのですが、とりあえずこうなりました*1:
simply_for: 2.1997323036193848sec. comprehension: 1.3124329090118407sec. range_case: 0.550351619720459sec. generator_case: 2.0610249996185304sec.
内包表記、うちのぱちょこんだと2倍も速くならないくらいですね。まあ速いことに変わりはありません。
それにしても、list(range(n))
が思った以上に速いですね…。
上の記事だと、単純な for より内包表記は約5倍ほど高速になってることを考えると、list(range) は10倍くらい速いことになりそう。
とはいえ、値を計算して…というのはできないですから、結局、内包表記使うのが速くていいですね。
あとは…、おかゆパソコンだと非常に微妙ですが、130msほどジェネレータつくってやるほうが速いですね。
内包表記はforが入れ子になると表記が煩わしくなるので、そういうときはジェネレータ作ってやると for よりは速いということになるのかもしれない(オーバーヘッドはあるだろうけど)。
からの入れ子3つ
というわけで、入れ子3つにしてみた:
def simply_for_2(n): a = list() for i in range(n): for j in range(n): for k in range(n): if i+j+k % 2 == 0: a.append(i+j+k) return a def comprehension_2(n): a = [i+j+k for i in range(n) for j in range(n) for k in range(n) if i+j+k % 2 == 0] return a def generator_2(n): for i in range(n): for j in range(n): for k in range(n): if i+j+k % 2 == 0: yield i+j+k def generator_case_2(n): a = list(generator_2(n)) return a if __name__ == '__main__': n = 300 for func in (simply_for_2, comprehension_2, generator_case_2): print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
simply_for_2: 8.179sec. comprehension_2: 7.936sec. generator_case_2: 7.853sec.
び、びみょ~~~~~~~~~~~~~~~( ᕦ 。 ᕤ)
入れ子が増えると内包表記もfor並にとろくなるのかしら。そう考えると、コードの見た目の観点から普通にfor書くべきだね。
ジェネレータは安定して、「微妙に」速い。
併用や!
見た目的にも、かつ、汎用性も兼ねることを考えると、ジェネレータと内包表記を併用するのが良さそう?
def generator_3(n): for i in range(n): for j in range(n): for k in range(n): yield i, j, k def generator_and_comprehension(n): a = [i+j+k for i, j, k in generator_3(n) if i+j+k % 2 == 0] return a if __name__ == '__main__': n = 250 for func in (simply_for_2, comprehension_2, generator_case_2, generator_and_comprehension): print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
simply_for_2: 4.445sec. comprehension_2: 4.476sec. generator_case_2: 4.380sec. generator_and_comprehension: 7.460sec.
ひえっ( ᕦ 。 ᕤ)お、おしょい・・・!
まあ恐らく原因は、forが実質1つ増えてるというところでしょうかな……。
ジェネレータで入れ子forを分離してやるというのは、個人的には見た目すごいスッキリして好きなんだけどな…。
ちなみに、
def generator_and_for(n): a = [] for i, j, k in generator_3(n): if i+j+k % 2 == 0: a.append(i+j+k) return a if __name__ == '__main__': n = 250 for func in (generator_and_comprehension, generator_and_for): print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
generator_and_comprehension: 7.505sec. generator_and_for: 7.465sec.
なので、もうジェネレータ律速って感じがしますね。内包表記の速みはどこにいったのか……?
itertoolsや!
from itertools import product def product_and_for(n): a = [] for i, j, k in product(range(n), range(n), range(n)): if i+j+k % 2 == 0: a.append(i+j+k) return a def product_and_comprehension(n): a = [i+j+k for i, j, k in product(range(n), range(n), range(n)) if i+j+k % 2 == 0] if __name__ == '__main__': n = 250 for func in (generator_and_comprehension, generator_and_for, product_and_for, product_and_comprehension): print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
いけー!ぷいきゅあがんばぇー!
generator_and_comprehension: 7.478sec. generator_and_for: 7.412sec. product_and_for: 4.933sec. product_and_comprehension: 4.959sec.
!!
普通に回すよりは若干遅くなるものの、product の力が伺えますね。これが標準ライブラリ(というかC)のちからか…!
というわけで
単一 for なら内包表記で、入れ子なら itertools.product 使って単一 for で回すのがキレイにそこそこ速そう。
10.1. itertools — 効率的なループ実行のためのイテレータ生成関数 — Python 3.5.2 ドキュメント
ちなみに、product(range(n), range(n), range(n))
は product(range(n), repeat=3)
とも書けます。
ちなみに
単一 for でも、回したいシーケンスが range では上手く表現できないときは、内包表記渡すかジェネレータ式渡すか、ですが、メモリ的なこと考えるとジェネレータ式渡したほうがよさそうではある?
面倒なのでやりませんけどね!😇
てか
なんでこんなに内包表記が遅くなったんでしょ?
def if_for(n): a = list() for i in range(n): if i % 2 == 0: a.append(i) return a def if_comprehension(n): a = [i for i in range(n) if i % 2 == 0] return a if __name__ == '__main__': n = 10**7 # for func in (generator_and_comprehension, generator_and_for, # product_and_for, product_and_comprehension): for func in (if_for, if_comprehension): print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
if_for: 2.967sec. if_comprehension: 2.430sec.
ふむふむ。内包表記 if
いれるだけでこんなに時間が接近するとは。
もしかして、入れ子がダメなのか?
def for_for(n): a = list() for i in range(n): for j in range(n): a.append(i+j) return a def comp_comprehension(n): a = [i+j for i in range(n) for j in range(n)] return a if __name__ == '__main__': n = 3500 # for func in (generator_and_comprehension, generator_and_for, # product_and_for, product_and_comprehension): for func in (for_for, comp_comprehension): print("{}: {:.3f}sec.".format(func.__name__, calc(func, 3, n)))
for_for: 3.454sec. comp_comprehension: 2.177sec.
ほほう🤔約1.5倍なので、これは最初にやった単一forと変わらないですね。
内包表記、if 入れると遅くなるのか……?
結論
if はヤバイ(?)
*1:桁を切れ