Pythonはrに比べると処理速度が速いと言われています。しかし、Julia、C/C++などに比べると”うわ、私の処理、遅すぎ、、?”と感じるかもしれません。今回は、Pyhonで処理速度が遅いと感じたら見直すべきいくつかのポイントをご紹介します。
- Point1:if xx in listはsetに変えるべし
- Point2:辞書の作成はdefaultdictを使うべし
- Point3:ローカル変数を使うべし
- Point4:関数アクセスでdot.は避けるべし
- Point5:変数置換は1行で済ませるべし
- Point6:文字列の結合はjoinを使うべし
- Point7:for文innerloopはできるだけ外側で済ますべし
- まとめ
- 参考
なお、本内容はこちらの記事を筆者が引用、改変したものです。
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気まぐれブログ