Pythonが遅いと感じたら!見直すべき高速化のポイント

Python高速

Pythonはrに比べると処理速度が速いと言われています。しかし、Julia、C/C++などに比べると”うわ、私の処理、遅すぎ、、?”と感じるかもしれません。今回は、Pyhonで処理速度が遅いと感じたら見直すべきいくつかのポイントをご紹介します。

なお、本内容はこちらの記事を筆者が引用、改変したものです。

https://towardsdatascience.com/10-techniques-to-speed-up-python-runtime-95e213e925dc

Point1:if xx in listはsetに変えるべし

ある要素がある配列に含むかどうかを判断させる場合は listを使うことがあります。その際にsetを使うと処理が高速になります。

listの場合

%%time
import random

randome_elements = random.sample(range(0, 10000000), 1000)
list_seq = list(range(100000)) #Point

counter = 0
for ele in randome_elements:
    if ele in list_seq:
        counter += 1

> Wall time: 2.08 s

setの場合

%%time
import random

randome_elements = random.sample(range(0, 10000000), 1000)
set_seq = set(range(100000)) #Point

counter = 0
for ele in randome_elements:
    if ele in set_seq:
        counter += 1

>Wall time: 20.5 ms

listの場合2秒程度かかっていた処理が、setに変えただけで20msに短縮されました!もちろん、判断している配列の長さは同じです。

print(len(list_seq))
print(len(set_seq))
>1000
>1000

Point2:辞書の作成はdefaultdictを使うべし

Pythonの辞書形は存在しないkeyを指定するとエラーになります。

dict_sample = {'dog': 5, 'cat' : 4}
print(dict_sample['dog'])
>5

print(dict_sample['rabbit'])
>KeyError                                  Traceback (most recent call last)
<ipython-input-122-8c9f0c1d00a5> in <module>
      2 print(dict_sample['dog'])
      3 
----> 4 print(dict_sample['rabbit'])

KeyError: 'rabbit'

そのためfor文などで、繰り返し辞書を作成する際には、はじめにそのkeyが辞書に存在するかを判断させる必要があります。しかし、colloctionモジュールのdefaultdictは、デフォルト値を設定できるため、keyの存在を確認する必要がなく、処理速度が速くなります。

通常のdict

%%time
import random
import string

rand_str = random.choices(string.ascii_letters, k=10000000)

wdict = {}  #Point
for s in rand_str:
    if s  in wdict:
            wdict[s] += 1
    else:
            wdict[s] = 1

>Wall time: 6.19 s

defaultdict

%%time
from collections import defaultdict
rand_str = random.choices(string.ascii_letters, k=10000000)

wdict = defaultdict(int)  #Point

for s in rand_str:
    wdict[s] += 1

>Wall time: 5.48 s

若干ですが速くなっています。defaultdictは他にも様々な使い方ができるので、気になる方は下部参考をご覧ください。

Point3:ローカル変数を使うべし

Pythonでは、同じ処理でもグローバル変数を使うより、関数内のローカル変数を使った方が処理が速くなります。

グローバル変数を使う

%%time
import math

size = 5000  
result = []

#Point
for x in range(size):
    for y in range(size):
        z = math.sqrt(x) + math.sqrt(y)
        result.append(z)

>Wall time: 16.3 s

ローカル変数にして使う

%%time
import math

#Point
def main():
    size = 5000  
    result = []
    for x in range(size):
        for y in range(size):
            z = math.sqrt(x) + math.sqrt(y)
            result.append(z)
    return result

result = main()

>Wall time: 13.8 s

同じ処理を関数にしただけで、高速になりましたね!

Point4:関数アクセスでdot.は避けるべし

importで読み込んだモジュールから関数を使う場合、.で繋げてアクセスします。しかし、直接importした方が速度は速くなります。先ほどの例で使ったsqrtを直接読み込んで使ってみます。

sqrtを直接import

%%time
from math import sqrt #Point


def main():
    size = 5000  
    result = []
    for x in range(size):
        for y in range(size):
            z = sqrt(x) + sqrt(y) 
            result.append(z)
    return result

result = main()
>Wall time: 13.2 s

さらにlistのappnedも変数に代入してしまいます。

%%time
from math import sqrt


def main():
    size = 5000  
    result = []
    append = result.append #Point
    sqrt = math.sqrt #Point
    
    for x in range(size):
        for y in range(size):
            z = sqrt(x) + sqrt(y)
            append(z)
    return result

result = main()

>Wall time: 8.9 s

.がfor文内にある場合は、上記を考慮すると速くなるかもしれません。

Point5:変数置換は1行で済ませるべし

Pythonは、複数の値を複数の変数に1行で代入することができます。これを用いると変数の入れ替えなどは、高速になります。

通常の置換

%%time
def main():
    size = 100000000
    for _ in range(size): 
        a = 3
        b = 5
        temp = a #Point
        a = b
        b = temp

main()
>Wall time: 6.72 s

1行で置換

%%time
def main():
    size = 100000000
    for _ in range(size):
        a = 3
        b = 5
        b, a = a, b #Point

main()
>Wall time: 6.34 s

この記法はPython独特ですが、積極的に使っていきたいですね。

Point6:文字列の結合はjoinを使うべし

pythonの文字列結合には、様々な方法がありますが、文字列が配列になっている場合は、joinを使うと高速になります。

+で結合

%%time
import string

def main():
    string_list = list(string.ascii_letters * 100)
    for _ in range(10000):
        result = ''
        for s in string_list:
             result += s #Point
result = main()
>Wall time: 6.77 s

joinで結合

%%time
import string
def main():
    string_list = list(string.ascii_letters * 100)
    for _ in range(10000):
        result = ''.join(string_list) #Point
main()
>Wall time: 505 ms

joinは配列で処理をするため、高速になりました。

Point7:for文innerloopはできるだけ外側で済ますべし

for文内にfor文を書くinnerloopの場合は、innerloopの外で処理ができる場合外側で処理を済ませましょう。

%%time
import math

def main():
    size = 10000
    sqrt = math.sqrt
    for x in range(size):
        for y in range(size):
            z = sqrt(x) + sqrt(y)  #Point

main() 
>Wall time: 24.1 s

sqrt(x)の処理を外側に出す

%%time
def main():
    size = 10000
    sqrt = math.sqrt
    for x in range(size):
        sqrt_x = sqrt(x)  #Point
        for y in range(size):
            z = sqrt_x + sqrt(y)

main() 
>Wall time: 16.9 s

innerloopの場合、何度も同じ計算を避けるように工夫することで速くなります。

まとめ

大規模データになると解析前の処理を行うだけで、多くの時間がかかる場合があります。できるだけ助長な記述を減らし、最短ルートで同じ結果を得られるように工夫したいですね。

※本記事は筆者が個人的に学んだこと感じたことをまとめた記事なります。所属する組織の意見・見解とは無関係です。

参考

defaultdict()についてわかりやすくまとめて見た - Kyam気まぐれブログ

Python defaultdict の使い方 - Qiita

Python中級者への道しるべ - Qiita