Pythonコードのパフォーマンス最適化の総合ガイド
Pythonは動的型付けのインタープリタ言語として、Cのような静的型付けのコンパイル言語と比較すると、実行速度が遅い場合があります。しかし、特定の技術と戦略を通じて、Pythonコードのパフォーマンスを大幅に向上させることができます。
この記事では、Pythonコードを最適化して、より高速かつ効率的に実行させる方法を探ります。Pythonのtimeit
モジュールを利用して、コードの実行時間を正確に測定します。
注意: デフォルトでは、timeit
モジュールはコードの実行を100万回繰り返して、測定結果の精度と安定性を確保します。
def print_hi(name):
print(f'Hi, {name}')
if __name__ == '__main__':
# print_hi('leapcell')メソッドを実行する
t = timeit.Timer(setup='from __main__ import print_hi', stmt='print_hi("leapcell")')
t.timeit()
Pythonスクリプトの実行時間を計算する方法
time
モジュールでは、time.perf_counter()
が高精度のタイマーを提供しており、短時間の間隔を測定するのに適しています。例えば:
import time
# プログラムの開始時間を記録する
start_time = time.perf_counter()
# あなたのコードロジック
#...
# プログラムの終了時間を記録する
end_time = time.perf_counter()
# プログラムの実行時間を計算する
run_time = end_time - start_time
print(f"プログラムの実行時間: {run_time} 秒")
I. I/O集中型操作
I/O集中型操作(Input/Output Intensive Operation)とは、実行時間の大部分を入出力操作の完了を待つことに費やすプログラムまたはタスクを指します。I/O操作には、ディスクからのデータ読み取り、ディスクへのデータ書き込み、ネットワーク通信などが含まれます。これらの操作は通常、ハードウェアデバイスを伴うため、その実行速度はハードウェア性能とI/O帯域幅に制限されます。
それらの特徴は以下の通りです:
- 待機時間: プログラムがI/O操作を実行する際、データが外部デバイスからメモリへ、またはメモリから外部デバイスへ転送されるのを待たなければならないことが多く、これによりプログラムの実行がブロックされる可能性があります。
- CPU利用率: I/O操作の待機時間のため、この期間中CPUがアイドル状態になり、CPU利用率が低くなることがあります。
- パフォーマンスのボトルネック: I/O操作の速度はしばしばプログラムのパフォーマンスのボトルネックとなり、特にデータ量が多い場合や転送速度が遅い場合に顕著です。
例えば、I/O集中型操作のprint
を使用して、それを100万回実行する場合:
import time
import timeit
def print_hi(name):
print(f'Hi, {name}')
return
if __name__ == '__main__':
start_time = time.perf_counter()
# print_hi('leapcell')メソッドを実行する
t = timeit.Timer(setup='from __main__ import print_hi', stmt='print_hi("leapcell")')
t.timeit()
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"プログラムの実行時間: {run_time} 秒")
実行結果は3秒です。
一方、I/O操作を使用しないメソッドを実行する場合、つまりprint()
を使用せずにprint_hi('xxxx')
という空のメソッドを呼び出すと、プログラムは著しく高速になります:
def print_hi(name):
# print(f'Hi, {name}')
return
I/O集中型操作の最適化方法
コード内で必要な場合、例えばファイルの読み書きなどでは、以下の方法を使用して効率を向上させることができます:
-
非同期I/O:
asyncio
のような非同期プログラミングモデルを使用します。これにより、プログラムはI/O操作の完了を待っている間に他のタスクを続けて実行でき、CPU利用率を向上させます。 - バッファリング: バッファを使用してデータを一時的に格納し、I/O操作の頻度を減らします。
- 並列処理: 複数のI/O操作を並列に実行して、全体のデータ処理速度を向上させます。
- データ構造の最適化: 適切なデータ構造を選択して、データの読み書き回数を減らします。
II. ジェネレータを使用してリストと辞書を生成する
Python 2.7以降のバージョンでは、リスト、辞書、セットのジェネレータに改善が加えられ、データ構造の構築プロセスがより簡潔かつ効率的になりました。
1. 従来の方法
def fun1():
list=[]
for i in range(100):
list.append(i)
if __name__ == '__main__':
start_time = time.perf_counter()
t = timeit.Timer(setup='from __main__ import fun1', stmt='fun1()')
t.timeit()
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"プログラムの実行時間: {run_time} 秒")
# 出力結果: プログラムの実行時間: 3.363 秒
2. ジェネレータを使用してコードを最適化する
注意: 以下の内容の便宜上、メイン関数main
のコード部分は省略します。
def fun1():
list=[ i for i in range(100)]
# プログラムの実行時間: 2.094 秒
上記の導出式プログラムからわかるように、この方法はより簡潔で理解しやすいだけでなく、高速でもあります。このため、この方法はリストとループの生成における優先的な方法となっています。
III. 文字列の連結を避け、join()
を使用する
join()
はPythonの文字列メソッドで、シーケンス内の要素を文字列に連結(または結合)するために使用され、通常は特定の区切り文字を使用します。その利点は通常以下の通りです:
-
高効率:
join()
は文字列の連結において効率的な方法であり、特に大量の文字列に対して有効です。通常、+
演算子や%
フォーマットを使用するよりも高速です。大量の文字列を連結する場合、join()
メソッドは1つずつ連結するよりもメモリを節約します。 -
簡潔性:
join()
はコードをより簡潔にし、繰り返しの文字列連結操作を避けます。 - 柔軟性: 任意の文字列を区切り文字として指定できるため、文字列の結合に大きな柔軟性を提供します。
- 幅広い適用範囲: 文字列だけでなく、リストやタプルなどのシーケンス型にも使用できます。ただし、要素は文字列に変換できる必要があります。
例えば:
def fun1():
obj=['hello','this','is','leapcell','!']
s=""
for i in obj:
s+=i
# プログラムの実行時間: 0.35186 秒
join()
を使用して文字列を連結する場合:
def fun1():
obj=['hello','this','is','leapcell','!']
"".join(obj)
# プログラムの実行時間: 0.1822 秒
join()
を使用することで、関数の実行時間が0.35秒から0.18秒に短縮されます。
IV. Map
を使用してループを置き換える
ほとんどのシナリオでは、従来のfor
ループは、より効率的なmap()
関数で置き換えることができます。map()
はPythonの組み込みの高階関数で、指定された関数をリスト、タプル、または文字列などのさまざまな反復可能なデータ構造に適用できます。map()
を使用する主な利点は、より簡潔で効率的なデータ処理方法を提供し、明示的なループコードの記述を避けることができることです。
従来のループ方法
def fun1():
arr=["hello", "this", "is", "leapcell", "!"]
new = []
for i in arr:
new.append(i)
# プログラムの実行時間: 0.3067 秒
map()
関数を使用して同じ機能を実現する
def fun2(x):
return x
def fun1():
arr=["hello", "this", "is", "leapcell", "!"]
map(fun2,arr)
# プログラムの実行時間: 0.1875 秒
比較すると、map()
を使用することでほぼ半分の時間を節約でき、実行効率が大幅に向上します。
V. 適切なデータ構造を選択する
適切なデータ構造を選択することは、Pythonコードの実行効率を向上させるために重要です。さまざまなデータ構造は、特定の操作に最適化されています。合理的な選択により、データの検索、追加、削除を高速化し、プログラムの全体的な操作効率を向上させることができます。
例えば、コンテナ内の要素を判断する場合、辞書の検索効率はリストよりも高いですが、これは大量のデータの場合です。少量のデータの場合は逆になります。
少量のデータでのテスト
def fun1():
arr=["hello", "this", "is", "leapcell", "!"]
'hello' in arr
'my' in arr
# プログラムの実行時間: 0.1127 秒
def fun1():
arr={"hello", "this", "is", "leapcell", "!"}
'hello' in arr
'my' in arr
# プログラムの実行時間: 0.1702 秒
numpy
を使用して100個の整数をランダムに生成する
import numpy as np
def fun1():
nums = {i for i in np.random.randint(100, size=100)}
1 in nums
# プログラムの実行時間: 14.28 秒
def fun1():
nums = {i for i in np.random.randint(100, size=100)}
1 in nums
# プログラムの実行時間: 13.53 秒
少量のデータの場合は、list
の実行効率がdict
よりも高いことがわかりますが、大量のデータの場合は、dict
の効率がlist
よりも高くなります。
頻繁な追加と削除操作があり、追加および削除される要素の数が多い場合、list
の効率は高くありません。この場合、collections.deque
を検討する必要があります。collections.deque
は両端キューで、スタックとキューの両方の特性を持ち、両端での挿入と削除操作を$O(1)$の複雑度で実行できます。
collections.deque
の使用方法
from collections import deque
def fun1():
arr=deque()# 空のdequeを作成する
for i in range(1000000):
arr.append(i)
# プログラムの実行時間: 0.0558 秒
def fun1():
arr=[]
for i in range(1000000):
arr.append(i)
# プログラムの実行時間: 0.06077 秒
list
の検索操作も非常に時間がかかります。list
内の特定の要素を頻繁に検索したり、これらの要素に順序付けてアクセスする必要がある場合、bisect
を使用してlist
オブジェクトの順序を維持し、その中で二分検索を行うことで、検索効率を向上させることができます。
VI. 不要な関数呼び出しを避ける
Pythonプログラミングにおいて、関数呼び出しの回数を最適化することはコードの効率向上に極めて重要です。過度な関数呼び出しは、オーバーヘッドを増加させるだけでなく、追加のメモリを消費する可能性があり、それによってプログラムの実行速度が低下します。パフォーマンスを向上させるためには、不要な関数呼び出しを減らし、複数の操作を1つにまとめることを試みるべきです。これにより、実行時間とリソース消費を削減することができます。このような最適化戦略は、より効率的で高速なコードを書くのに役立ちます。
VII. 不要なimport
を避ける
Pythonのimport
文は比較的高速ですが、各import
にはモジュールの検索、モジュールコードの実行(まだ実行されていない場合)、そしてモジュールオブジェクトを現在の名前空間に配置する操作が含まれます。これらの操作には一定の時間とメモリが必要です。不要にモジュールをインポートすると、これらのオーバーヘッドが増加します。
VIII. グローバル変数の使用を避ける
import math
size=10000
def fun1():
for i in range(size):
for j in range(size):
z = math.sqrt(i) + math.sqrt(j)
# プログラムの実行時間: 15.6336 秒
多くのプログラマーは最初、Python言語でいくつかの簡単なスクリプトを書きます。スクリプトを書く際には、通常、上記のコードのように直接グローバル変数として書くことが多いです。しかし、グローバル変数とローカル変数の実装方法が異なるため、グローバルスコープで定義されたコードは関数内で定義されたコードよりもはるかに遅く実行されます。スクリプト文を関数内に入れることで、通常15% - 30%の速度向上を達成することができます。
import math
def fun1():
size = 10000
for i in range(size):
for j in range(size):
z = math.sqrt(i) + math.sqrt(j)
# プログラムの実行時間: 14.9319 秒
IX. モジュールと関数の属性アクセスを避ける
import math # 推奨されない
def fun2(size: int):
result = []
for i in range(size):
result.append(math.sqrt(i))
return result
def fun1():
size = 10000
for _ in range(size):
result = fun2(size)
# プログラムの実行時間: 10.1597 秒
.
(属性アクセス演算子)を使用するたびに、__getattribute__()
や__getattr__()
などの特定のメソッドがトリガーされます。これらのメソッドは辞書操作を行うため、追加の時間オーバーヘッドをもたらします。from import
文を使用することで、属性アクセスを排除することができます。
from math import sqrt # 推奨: 必要なモジュールのみをインポートする
def fun2(size: int):
result = []
for i in range(size):
result.append(sqrt(i))
return result
def fun1():
size = 10000
for _ in range(size):
result = fun2(size)
# プログラムの実行時間: 8.9682 秒
X. 内側のfor
ループ内の計算を減らす
import math
def fun1():
size = 10000
sqrt = math.sqrt
for x in range(size):
for y in range(size):
z = sqrt(x) + sqrt(y)
# プログラムの実行時間: 14.2634 秒
上記のコードでは、sqrt(x)
は内側のfor
ループ内にあり、ループが実行されるたびに再計算され、不要な時間オーバーヘッドが追加されます。
import math
def fun1():
size = 10000
sqrt = math.sqrt
for x in range(size):
sqrt_x=sqrt(x)
for y in range(size):
z = sqrt_x + sqrt(y)
# プログラムの実行時間: 8.4077 秒
Leapcell: Pythonアプリケーションをホストするための最高のサーバーレスプラットフォーム
最後に、Pythonアプリケーションをデプロイするための最高のプラットフォームLeapcellを紹介します。
1. 多言語サポート
- JavaScript、Python、Go、またはRustで開発できます。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に応じて課金されます — リクエストがなければ、料金はかかりません。
3. 比類のない費用対効果
- 従量課金制で、アイドル料金はありません。
- 例: 平均応答時間60msで694万回のリクエストを$25でサポートします。
4. 簡素化された開発者体験
- 直感的なUIで簡単にセットアップできます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- アクション可能な洞察を得るためのリアルタイムメトリクスとロギング。
5. 簡単なスケーラビリティと高いパフォーマンス
- 高い同時アクセスを簡単に処理するための自動スケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。
LeapcellのTwitter: https://x.com/LeapcellHQ!