forループ内でリストに要素を追加するような処理を実装する際に、素直にappendメソッドを使用するのは抵抗がある。なぜなら繰り返し処理で処理速度が遅いと分かっている方法を採用したくないからだ。
さて、どのような方法があるだろうか。
開発環境:
Python 3.9.2
参考情報:
リスト内包表記、append、extendの計測用の関数を用意した。
import timeit
import statistics
def make_data_1(num):
num_list = [i for i in range(num)]
return num_list
def make_data_2(num):
num_list = []
append_num = num_list.append # ここがポイント
for i in range(num):
append_num(i)
return num_list
def make_data_3(num):
num_list = []
for i in range(num):
num_list.extend([i]) # 要素ひとつだけのリストのextendを繰り返す・・・?
return num_list
これをtimeitで計測する。
>>> num = 1000000
>>>
>>> dulation1 = timeit.repeat('make_data_1(num)',\
... globals=globals(), number=1, repeat=10)
>>> dulation2 = timeit.repeat('make_data_2(num)',\
... globals=globals(), number=1, repeat=10)
>>> dulation3 = timeit.repeat('make_data_3(num)',\
... globals=globals(), number=1, repeat=10)
>>>
>>> do_stat(dulation1)
mean: 0.1646276962000001, median: 0.15911065050000017
>>> do_stat(dulation2)
mean: 0.20910862620000037, median: 0.20552880600000112
>>> do_stat(dulation3)
mean: 0.39362302230000046, median: 0.3918427175000003
>>>
リスト内包表記が速いのは周知の事実だが、appendも遅くなく、寧ろextendの遅さが際立っている。実はこのappendはオブジェクト化した後にそれを参照しているため高速化されているのだ。ではオブジェクト化しないappendはどうだろうか。
>>> def make_data_4(num):
... num_list = []
... for i in range(num):
... num_list.append(i) # おなじみの使い方だ
... return num_list
...
>>> dulation4 = timeit.repeat('make_data_4(num)',\
... globals=globals(), number=1, repeat=10)
>>>
>>> do_stat(dulation4)
mean: 0.2676781162000001, median: 0.2674647229999998 # オブジェクト化した場合より遅い
では先のextendの遅さは何だろうか。基本に忠実にextendを使うとすると下記のようになる。
>>> def make_data_5(num):
... num_list = []
... num_list.extend(i for i in range(num)) # extendにはiterableを渡せば良いためgeneratorを生成して渡そう
... return num_list
...
>>> dulation5 = timeit.repeat('make_data_5(num)',\
... globals=globals(), number=1, repeat=10)
>>>
>>> do_stat(dulation5)
mean: 0.20951362700000012, median: 0.20629997800000055 # 通常のappendよりは速くなった
appendやextendを使うことを避けるために、予め必要な長さのリストを用意してから値を再代入する実装も良くある。
>>> def make_data_6(num):
... num_list = [None] * num # ここがポイント
... for i in range(num):
... num_list[i] = i
... return num_list
...
>>> dulation6 = timeit.repeat('make_data_6(num)',\
... globals=globals(), number=1, repeat=10)
>>>
>>> do_stat(dulation6)
mean: 0.17822860669999904, median: 0.1780844179999992 # 2番目に速い
>>>
ついでに。Pythonを忘れた状態でコーディングするとこのようなことをやりがちだ。
>>> words = ['boo', 'bar', 'baz', 'qux']
>>> words.extend('quux')
>>> words
['boo', 'bar', 'baz', 'qux', 'q', 'u', 'u', 'x']
まとめ:
同じメソッドを使っても書き方次第で速度が変わる
憶測や先入観で判断せず試験してより良い方法を選択しよう
しっくりこない時は、別の観点からアプローチ可能か検討してみよう