Pythonで同じ動作を実現させるのに複数通りの書き方が考えられる場合、どれを選ぶかは可読性と速度のバランスを考えて選びたいところ。そこで前から気になっていたコードの速度をいくつか比較してみた。
環境
- Google Colaboratory
- model: Intel(R) Xeon(R) CPU @ 2.20GHz (×2)
- Python version: 3.6.9 (default, Nov 7 2019, 10:44:02)
[GCC 8.3.0]
測定方法
%%timeit
を利用
問題1: リスト生成
Pythonでlist.append()
でリストを生成するのは遅いよという有名な話。以降、N = 1000000
としている。
a = []
for i in range(N):
a.append(i)
10 loops, best of 3: 96.9 ms per loop
メソッドの名前解決に地味な時間がかかっているので、変数にメソッドを代入しておくと多少若干速くなる。可読性は悪いが。
a = []
append = a.append
for i in range(N):
append(i)
10 loops, best of 3: 70.2 ms per loop
また、list.append()
は一定のサイズを超えるたびに新たなメモリを確保する(allocate)手間が発生する。そこであらかじめ長さを決めておくと若干速くなる。ただし上の方法よりも必ずしも速くなるとは限らない無い気がする。
a = [None] * N
for i in range(N):
a[i] = i
10 loops, best of 3: 64.3 ms per loop
そして速いと評判なのが次のリスト内包表記である。記述も簡単なのでメリットは大きい。
a = [i for i in range(N)]
10 loops, best of 3: 58.6 ms per loop
もっとも、この例のようなことをやりたければlist()
を使えば終わってしまうのだが。
a = list(range(N))
10 loops, best of 3: 35.9 ms per loop
問題2: map VS ジェネレータ式
この2つは機能がかなり似ている。ジェネレータ式は全てmap()
かreduce()
に書き換え可能な気がしている。
言うまでもなく、map()
とジェネレータ式はどちらもイテレータを生成するのだが、イテレータ生成は一瞬で終わってしまうので、後段の処理も含めて速度を比較する。
組み込み関数+リスト生成
組み込み関数str()
をrange
オブジェクトに適用し、リストを生成する。
まずは素朴にmap
をlist()
で変換する。
a = list(map(str, range(N)))
10 loops, best of 3: 184 ms per loop
違和感はあるが、リテラルとアンパックを利用する方法もある。速度は変わらなかった。
a = [*map(str, range(N))]
10 loops, best of 3: 183 ms per loop
次はリスト内包表記との比較。これはジェネレータ式では無い気もするので、強引にジェネレータ式を使った場合も一応やってみた。結果はどちらもmap
より遅い!まさかmap
がリスト内包表記に勝つとは...
内包表記
a = [str(i) for i in range(N)]
1 loop, best of 3: 211 ms per loop
ジェネレータ式
a = list(str(i) for i in range(N))
1 loop, best of 3: 233 ms per loop
組み込み関数 + join()
続いてはイテレータを'str.join()'メソッドに渡す場合で測定。
map
a = ''.join(map(str, range(N)))
1 loop, best of 3: 201 ms per loop
ジェネレータ式
a = ''.join(str(i) for i in range(N))
1 loop, best of 3: 262 ms per loop
やはり、map()
の勝利...。ちなみに、join()
の引数をリストにしてみると面白い結果になる。通常、イテレータで済むところをわざわざリストに変換して関数に渡すと遅くなるはずである。しかし一部の関数については謎の最適化が発動し、リストを渡す方が速くなったりするのだ。
map
a = ''.join(list(map(str, range(N))))
1 loop, best of 3: 199 ms per loop
リスト内包表記
a = ''.join([str(i) for i in range(N)])
1 loop, best of 3: 242 ms per loop
なんと、map()
の場合は全然変わらない!ジェネレータ式→リスト内包表記の場合は私の予想通り若干速くなった。list()
にかかる時間がjoin()
の僅かな高速化を打ち消したとでも言うのだろうか?一体何が起こっているPython...
自作関数 + join()
組み込み関数を次のように定義しなおしてどの程度遅くなるか調べる。
def tostr(x):
return str(x)
map
a = ''.join(map(tostr, range(N)))
1 loop, best of 3: 277 ms per loop
ラムダ式を渡した場合
a = ''.join(map(lambda x: str(x), range(N)))
1 loop, best of 3: 292 ms per loop
ジェネレータ式
a = ''.join(tostr(i) for i in range(N))
1 loop, best of 3: 331 ms per loop
どれも順当に遅くなっただけで序列は変わらないすね....。ちなみに、この場合に限り自作関数をジェネレータ関数に書き換えてfor文回す方が、ジェネレータ式よりは速い。自作関数を毎ループ呼び出すよりは最初にジェネレータを生成して回す方が良いということであろう。まぁ、map()
の方が速いですけどね。
演算式 + リスト生成
map()
は必ず関数を渡さなければならないが、ジェネレータ式はその必要が無い。例えば演算式をそのままmap()
に渡すことはできない。
def mod13(x):
return x % 13
無理やりmap
a = list(map(mod13, range(N)))
10 loops, best of 3: 95.4 ms per loop
リスト内包表記
a = [x % 13 for x in range(N)]
10 loops, best of 3: 59.5 ms per loop
ジェネレータ式
a = list(x % 13 for x in range(N))
10 loops, best of 3: 79.2 ms per loop
自作関数 + リスト内包表記
%%timeit
a = [mod13(x) for x in range(N)]
10 loops, best of 3: 120 ms per loop
メソッド + リスト生成
この場合もmap()
は自作関数が必要である。次のようにして上の場合と同じような実験を行った。
lis = list(map(str, range(N)))
def rstrip(x):
return x.rstrip('0')
以降のコードはrange(N)
をlis
に、mod13
をrstrip
に置き換えたものであり、結果は同じ序列になったので省略する。Pythonは自作関数へのアクセスが遅いようである。
(おまけ) 問題3: print()
文字列のリストの各要素をprint()
するときは、for文を回すよりは'join()'を使う方が圧倒的に速いぞ(当たり前)
a = list(map(str, range(1000)))
%%time
for x in a:
print(x, end=' ')
Wall time: 96.5 ms
%%time
print(' '.join(a), end=' ')
Wall time: 83.4 µs
オプション引数sep
とアンパックを使う手も一応ある。全要素が文字列になってないときは出番かもしれないが、for文で回すのとあまり変わらない。
%%time
print(*a, sep=' ', end=' ')
Wall time: 94.4 ms
結論
- 自作関数の呼び出しはとても遅い。メソッド呼び出しもやや遅い。
- 組み込み関数だけで済むときは
map()
- 自作関数を使わざるを得ないときも
map()
-
map()
,filter()
を組み合わせたような複雑な処理をしたいときはジェネレータ関数
-
- それ以外はジェネレータ式