この記事は,Pythonを少しでも高速に実行するための方法をまとめたTips集です.

随時更新予定です.

グローバル名前空間で大きな処理を書かない

Pythonではメソッド内に処理を書かずにグローバル名前空間に処理を書くこともできます。 Pythonは変数アクセスを名前空間ごとの辞書(ハッシュマップみたいなもの)で検索することで実現しています。 グローバル名前空間の辞書にはデフォルトの状態でいくつかの要素が追加されているので、メソッド内でローカル変数にアクセスする場合に比べて変数アクセスのコストが重くなります。

なので、グローバル名前空間でforループなどを回すと変数アクセスのコストが無視できなくなるので、forループを回すなどの大きな処理を行う場合はメソッド内に処理を書くようにしましょう。

グローバル名前空間にforループの処理をべた書きした場合と、メソッド内に同じ処理を書いた場合での実行時間を比較してみると、こんな感じになります。

  • グローバル名前空間にべた書きした場合
start = time.time()
for k in range(0, 30000):
    for l in range(0, 30000):
        pass
elapsed_time = time.time() - start
print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:24.14781355857849[sec]
  • メソッド内に処理を書いた時
def loop():
    for i in range(0, 30000):
        for j in range(0, 30000):
            pass

start = time.time()
loop()
elapsed_time = time.time() - start
print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:12.459148168563843[sec]

倍ぐらい違います。

配列の初期化はリスト内包表記で

普通のプログラミング言語っぽく配列を初期化すると

array = []
for i in range(10):
    array.append(i)

のような形になりますが、Pythonではリスト内包表記という書き方によって

array = [i for i in range(10)]

と書くことができます。

リスト内包表記の場合、appendの参照や呼び出しがなくなるらしいので、その分高速になるようです。 リスト内包表記で書ける場合はがんがん使っていきましょう。

実行速度を比較してみるとこんな感じです。

def initialize_array1():
    array = []
    for i in range(10000000):
        array.append(i)

start = time.time()
initialize_array1()
elapsed_time = time.time() - start
print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:0.9040536880493164[sec]
def initialize_array2():
    array = [i for i in range(10000000)]

start = time.time()
initialize_array2()
elapsed_time = time.time() - start
print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:0.5871231555938721[sec]

配列の要素がすべて同じ場合は*演算子を使う

上で配列の初期化はリスト内包表記で書くほうが早いと記述しましたが,配列をすべて同じ値で初期化する場合,*演算子を用いて以下のように書いたほうが格段に高速化できます.

array = [0] * 10000000

実際,実行速度を比較してみると,リスト内包表記より5倍ほど高速なことがわかります. 配列の要素がすべて同じではない場合や,2次元以上の配列ではこの書き方はできないため,やや特殊なケース限定となってしまいますが,それでもたまにはあるケースだと思うので,頭の片隅に入れておくと良いかもしれません.

def initialize_array1():
    array = []
    for _ in range(10000000):
        array.append(0)

start = time.time()
initialize_array1()
elapsed_time = time.time() - start
print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:0.835083007812[sec]
def initialize_array2():
    array = [0 for _ in range(10000000)]

start = time.time()
initialize_array2()
elapsed_time = time.time() - start
print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:0.4093458652496338[sec]
def initialize_array3():
    array = [0] * 10000000

start = time.time()
initialize_array3()
elapsed_time = time.time() - start
print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:0.07808589935302734[sec]

属性へのアクセスをローカル変数へのアクセスに切り替える

Pythonでは、モジュールやオブジェクトに存在する変数・メソッドを属性と呼びます。 例えば、mathモジュールではmath.ceil(5)といった感じで属性であるメソッドを呼び出すことができます。 しかし、これではモジュールへのアクセスに加えて属性へのアクセスも行われるため、ややコストが重くなります。

そこで、対象の属性を一時的にローカル変数へと格納してそっちにアクセスするようにします。 これによって、無駄なアクセスを抑えることができます。

def loop_use_attribute():
    for i in range(10000000):
        math.ceil(i)

start = time.time()
loop_use_attribute()
elapsed_time = time.time() - start
print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:0.8091247081756592[sec]
def loop_use_variable():
    ceil = math.ceil
    for i in range(10000000):
        ceil(i)

start = time.time()
loop_use_variable()
elapsed_time = time.time() - start
print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:0.5344369411468506[sec]

並列処理で高速化を狙うときはmultiprocessingモジュールを使う

Pythonで並列処理を行うときは、threadingモジュールかmultiprocessingモジュールを用いることが多いです。

threadingモジュールでは以下のようにしてスレッドを生成、実行します。 threadingモジュールによって、マルチスレッドで並列処理を実現することができます。

def func():
    pass

thread = threading.Thread(target=func)
thread.start()
thread.join()

一方で、multiprocessingモジュールは以下のようにしてプロセスを生成、実行します。 こちらはマルチプロセスで並列処理を実現することができます。

def func():
    pass

process = multiprocessing.Process(target=func)
process.start()
process.join()

Pythonでは、グローバルインタプリタロック(GIL)という排他制御が採用されているため、1つのプロセスに対して1つの処理しか実行することができません。 そのため、threadingモジュールを使ってマルチスレッドっぽいコードを書いたとしてもプロセスが1つなので処理が並列して行われません。

multiprocessingモジュールを使ってプロセスを複数に分けると実質的にGILを無視することができるため、並列処理によって高速化を狙っている場合はこのモジュールを用いると良いでしょう。 ただし、変数などがスレッドセーフでなくなるため、それを気にして実装を行う必要が出てきます。

def loop():
    for i in range(10000000):
        pass

if __name__ == '__main__':
    start = time.time()
    threads = [threading.Thread(target=loop) for _ in range(4)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
    elapsed_time = time.time() - start
    print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:0.5655884742736816[sec]
def loop():
    for i in range(10000000):
        pass

if __name__ == '__main__':
    start = time.time()
    processes = [multiprocessing.Process(target=loop) for _ in range(4)]
    for process in processes:
        process.start()
    for process in processes:
        process.join()
    elapsed_time = time.time() - start
    print("elapsed_time:{0}[sec]".format(elapsed_time))

>>> elapsed_time:0.2527484893798828[sec]