記事のソース: 新人のための Python の学習
みなさん、こんにちは。タオ兄弟です。今日は、Python の実行を高速化するための 8 つのクールな操作を紹介します。全文は合計 7,000 ワードあり、読むのに約 10 分かかります。
Python はスクリプト言語ですが、C/C++ などのコンパイル言語と比較すると、効率とパフォーマンスの点でいくつかの欠点があります。ただし、Python の効率が想像ほど誇張されていない場合もよくあります。この記事では、Python コードの実行を高速化するためのヒントをいくつかまとめます。
0
コード最適化ルール
この記事では、Python コードの実行を高速化するためのさまざまなテクニックを紹介します。コードの最適化の詳細に入る前に、コードの最適化の基本原則をいくつか理解する必要があります。
最初の基本原則: 時期尚早に最適化しない
多くの人はパフォーマンスの最適化を目的としてコードを書き始めますが、「高速なプログラムを正確に作成するよりも、より高速に正しいプログラムを作成する方がはるかに簡単です。」したがって、最適化の前提条件は、コードが適切に動作できることです。時期尚早に最適化すると、全体的なパフォーマンス指標の把握が無視される可能性があります。全体的な結果を得る前に優先順位を逆転させないでください。
2 番目の基本原則: 最適化のコストを秤量する
最適化にはコストがかかり、パフォーマンスの問題をすべて解決することはほぼ不可能です。通常直面する選択は、空間のための時間か、時間のための空間です。さらに、開発コストも考慮する必要があります。
第三の原則: 重要でない部分は最適化しない
コードのすべての部分が最適化された場合、これらの変更によりコードが読みにくくなり、理解しにくくなります。コードの実行が遅い場合は、まずコードが遅い箇所 (通常は内部ループ) を見つけて、遅い箇所の最適化に重点を置きます。他の場所では、多少の時間のロスはほとんど意味がありません。
1
グローバル変数を避ける
# 不推荐写法。代码耗时:26.8秒
import math
size = 10000
for x in range(size):
for y in range(size):
z = math.sqrt(x) + math.sqrt(y)
多くのプログラマは、まず Python 言語でいくつかの簡単なスクリプトを作成しますが、スクリプトを作成するときは、通常、上記のコードのようにグローバル変数として直接スクリプトを記述することに慣れています。ただし、グローバル変数とローカル変数の実装が異なるため、グローバル スコープで定義されたコードは、関数で定義されたコードよりも実行速度が大幅に遅くなります。スクリプト ステートメントを関数に組み込むことで、通常は 15% ~ 30% の速度向上を達成できます。
# 推荐写法。代码耗时:20.6秒
import math
def main(): # 定义到函数中,以减少全部变量使用
size = 10000
for x in range(size):
for y in range(size):
z = math.sqrt(x) + math.sqrt(y)
main()
2
部分的なアクセスを避ける
2.1
モジュールおよび関数の属性へのアクセスを回避する
# 不推荐写法。代码耗时:14.5秒
import math
def computeSqrt(size: int):
result = []
for i in range(size):
result.append(math.sqrt(i))
return result
def main():
size = 10000
for _ in range(size):
result = computeSqrt(size)
main()
(属性アクセス演算子)を使用するたびに、や など.
の特定のメソッドがトリガーされ、これらのメソッドは辞書操作を実行するため、追加の時間オーバーヘッドが発生します。ステートメントを使用すると、プロパティへのアクセスを排除できます。__getattribute__()
__getattr__()
from import
# 第一次优化写法。代码耗时:10.9秒
from math import sqrt
def computeSqrt(size: int):
result = []
for i in range(size):
result.append(sqrt(i)) # 避免math.sqrt的使用
return result
def main():
size = 10000
for _ in range(size):
result = computeSqrt(size)
main()
セクション 1 で、ローカル変数はグローバル変数よりも検索が高速であると述べました。そのため、頻繁にアクセスされる変数についてはsqrt
、ローカル変数に変更することで処理を高速化できます。
# 第二次优化写法。代码耗时:9.9秒
import math
def computeSqrt(size: int):
result = []
sqrt = math.sqrt # 赋值给局部变量
for i in range(size):
result.append(sqrt(i)) # 避免math.sqrt的使用
return result
def main():
size = 10000
for _ in range(size):
result = computeSqrt(size)
main()
さらにmath.sqrt
、computeSqrt
function には別のもの.
、つまり呼び出しlist
メソッドがありますappend
。メソッドをローカル変数に代入することで、computeSqrt
関数内のfor
ループ内での使用を完全に排除できます.
。
# 推荐写法。代码耗时:7.9秒
import math
def computeSqrt(size: int):
result = []
append = result.append
sqrt = math.sqrt # 赋值给局部变量
for i in range(size):
append(sqrt(i)) # 避免 result.append 和 math.sqrt 的使用
return result
def main():
size = 10000
for _ in range(size):
result = computeSqrt(size)
main()
2.2
クラス内プロパティへのアクセスを避ける
# 不推荐写法。代码耗时:10.4秒
import math
from typing import List
class DemoClass:
def __init__(self, value: int):
self._value = value
def computeSqrt(self, size: int) -> List[float]:
result = []
append = result.append
sqrt = math.sqrt
for _ in range(size):
append(sqrt(self._value))
return result
def main():
size = 10000
for _ in range(size):
demo_instance = DemoClass(size)
result = demo_instance.computeSqrt(size)
main()
回避の原則は.
クラス内プロパティにも適用され、アクセスはself._value
ローカル変数にアクセスするよりも遅くなります。頻繁にアクセスされるクラス内プロパティをローカル変数に割り当てることで、コードの実行速度を向上させることができます。
# 推荐写法。代码耗时:8.0秒
import math
from typing import List
class DemoClass:
def __init__(self, value: int):
self._value = value
def computeSqrt(self, size: int) -> List[float]:
result = []
append = result.append
sqrt = math.sqrt
value = self._value
for _ in range(size):
append(sqrt(value)) # 避免 self._value 的使用
return result
def main():
size = 10000
for _ in range(size):
demo_instance = DemoClass(size)
demo_instance.computeSqrt(size)
main()
3
不必要な抽象化を避ける
# 不推荐写法,代码耗时:0.55秒
class DemoClass:
def __init__(self, value: int):
self.value = value
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, x: int):
self._value = x
def main():
size = 1000000
for i in range(size):
demo_instance = DemoClass(size)
value = demo_instance.value
demo_instance.value = i
main()
追加の処理レイヤー (デコレーター、プロパティ アクセス、記述子など) でコードをラップすると、コードの速度が遅くなります。ほとんどの場合、プロパティ アクセサーの使用の定義を再検討する必要があり、getter/setter
関数を使用してプロパティにアクセスすることは、通常、C/C++ プログラマの従来のコーディング スタイルです。本当に必要ない場合は、単純な属性を使用してください。
# 推荐写法,代码耗时:0.33秒
class DemoClass:
def __init__(self, value: int):
self.value = value # 避免不必要的属性访问器
def main():
size = 1000000
for i in range(size):
demo_instance = DemoClass(size)
value = demo_instance.value
demo_instance.value = i
main()
4
データの重複を避ける
4.1
無意味なデータの重複を避ける
# 不推荐写法,代码耗时:6.5秒
def main():
size = 10000
for _ in range(size):
value = range(size)
value_list = [x for x in value]
square_list = [x * x for x in value_list]
main()
上記のコードではvalue_list
まったく不要であり、不要なデータ構造やコピーが作成されることになります。
# 推荐写法,代码耗时:4.8秒
def main():
size = 10000
for _ in range(size):
value = range(size)
square_list = [x * x for x in value] # 避免无意义的复制
main()
もう 1 つの状況は、Python のデータ共有メカニズムについて過度に偏執的であり、Python のメモリ モデルをよく理解していない、または信頼しておらず、copy.deepcopy()
そのような機能を悪用していることです。通常、これらのコードではコピー操作を省略できます。
4.2
値を交換するときに中間変数は使用されません
# 不推荐写法,代码耗时:0.07秒
def main():
size = 1000000
for _ in range(size):
a = 3
b = 5
temp = a
a = b
b = temp
main()
上記のコードは、値を交換するときに一時変数を作成しますtemp
。中間変数の助けを借りない場合、コードはより簡潔になり、より高速に実行されます。
# 推荐写法,代码耗时:0.06秒
def main():
size = 1000000
for _ in range(size):
a = 3
b = 5
a, b = b, a # 不借助中间变量
main()
4.3
文字列を連結するときは + の代わりに join を使用します
# 不推荐写法,代码耗时:2.6秒
import string
from typing import List
def concatString(string_list: List[str]) -> str:
result = ''
for str_i in string_list:
result += str_i
return result
def main():
string_list = list(string.ascii_letters * 100)
for _ in range(10000):
result = concatString(string_list)
main()
文字列を連結する場合a + b
、Python の文字列は不変オブジェクトであるため、メモリ空間に適用され、 と がa
それぞれb
新しく割り当てられたメモリ空間にコピーされます。そのため、文字列を結合したい場合はn
中間結果が生成されn-1
、それぞれの中間結果をメモリに適用してコピーする必要があり、作業効率に大きく影響します。連結文字列を使用する場合join()
、適用する必要がある合計メモリ領域が最初に計算され、次に必要なメモリが一度にすべて適用され、各文字列要素がメモリにコピーされます。
# 推荐写法,代码耗时:0.3秒
import string
from typing import List
def concatString(string_list: List[str]) -> str:
return ''.join(string_list) # 使用 join 而不是 +
def main():
string_list = list(string.ascii_letters * 100)
for _ in range(10000):
result = concatString(string_list)
main()
5
if条件の短絡特性を利用する
# 不推荐写法,代码耗时:0.05秒
from typing import List
def concatString(string_list: List[str]) -> str:
abbreviations = {'cf.', 'e.g.', 'ex.', 'etc.', 'flg.', 'i.e.', 'Mr.', 'vs.'}
abbr_count = 0
result = ''
for str_i in string_list:
if str_i in abbreviations:
result += str_i
return result
def main():
for _ in range(10000):
string_list = ['Mr.', 'Hat', 'is', 'Chasing', 'the', 'black', 'cat', '.']
result = concatString(string_list)
main()
if
条件の短絡特性は、if a and b
このようなステートメントの場合、 when a
is がFalse
計算なしで直接返されることb
、if a or b
そのようなステートメントのa
whenTrue
is が計算なしで直接返されることを意味しますb
。したがって、実行時間を節約するには、forステートメントのor
値のTrue
確率が高い変数を最初に書き込み、後回しにする必要があります。or
and
# 推荐写法,代码耗时:0.03秒
from typing import List
def concatString(string_list: List[str]) -> str:
abbreviations = {'cf.', 'e.g.', 'ex.', 'etc.', 'flg.', 'i.e.', 'Mr.', 'vs.'}
abbr_count = 0
result = ''
for str_i in string_list:
if str_i[-1] == '.' and str_i in abbreviations: # 利用 if 条件的短路特性
result += str_i
return result
def main():
for _ in range(10000):
string_list = ['Mr.', 'Hat', 'is', 'Chasing', 'the', 'black', 'cat', '.']
result = concatString(string_list)
main()
6
ループの最適化
6.1
while ループの代わりに for ループを使用する
# 不推荐写法。代码耗时:6.7秒
def computeSum(size: int) -> int:
sum_ = 0
i = 0
while i < size:
sum_ += i
i += 1
return sum_
def main():
size = 10000
for _ in range(size):
sum_ = computeSum(size)
main()
Python のfor
ループはwhile
ループよりもはるかに高速です。
# 推荐写法。代码耗时:4.3秒
def computeSum(size: int) -> int:
sum_ = 0
for i in range(size): # for 循环代替 while 循环
sum_ += i
return sum_
def main():
size = 10000
for _ in range(size):
sum_ = computeSum(size)
main()
6.2
明示的な for ループではなく暗黙的な for ループを使用する
上記の例では、さらに一歩進んで、明示的なループfor
の代わりに暗黙的なループを使用できます。for
# 推荐写法。代码耗时:1.7秒
def computeSum(size: int) -> int:
return sum(range(size)) # 隐式 for 循环代替显式 for 循环
def main():
size = 10000
for _ in range(size):
sum = computeSum(size)
main()
6.3
内部の for ループの計算を削減します。
# 不推荐写法。代码耗时:12.8秒
import math
def main():
size = 10000
sqrt = math.sqrt
for x in range(size):
for y in range(size):
z = sqrt(x) + sqrt(y)
main()
上記のコードはsqrt(x)
内部ループにありfor
、各トレーニング プロセス中に再計算されるため、時間のオーバーヘッドが増加します。
# 推荐写法。代码耗时:7.0秒
import math
def main():
size = 10000
sqrt = math.sqrt
for x in range(size):
sqrt_x = sqrt(x) # 减少内层 for 循环的计算
for y in range(size):
z = sqrt_x + sqrt(y)
main()
7
numba.jitの使用
上で紹介した例に従い、これに基づいて使用しますnumba.jit
。numba
Python 関数をマシンコードに JIT コンパイルして実行できるため、コードの実行速度が大幅に向上します。の詳細についてnumba
は、以下のホームページを参照してください。
http://numba.pydata.org/numba.pydata.org
# 推荐写法。代码耗时:0.62秒
import numba
@numba.jit
def computeSum(size: float) -> int:
sum = 0
for i in range(size):
sum += i
return sum
def main():
size = 10000
for _ in range(size):
sum = computeSum(size)
main()
8
適切なデータ構造を選択する
Python の組み込みデータ構造 ( str
、tuple
、 、などlist
)はすべて C で最下位レベルで実装されており、非常に高速ですが、新しいデータ構造を自分で実装してパフォーマンスの面で組み込みのset
速度を達成することはほとんど不可能です。dict
list
C++ と同様にstd::vector
、これは動的配列です。一定量のメモリ空間が事前に割り当てられます。事前に割り当てられたメモリ空間が使い果たされ、そこに要素が追加されると、より大きなメモリ空間が適用され、元の要素がすべてそこにコピーされ、その後、以前のメモリ、新しい要素を挿入する前のスペース。要素を削除する場合も同様で、使用されているメモリ空間が事前に割り当てられたメモリ空間の半分未満の場合、追加の小さなメモリが適用され、要素のコピーが作成され、元の大きなメモリ空間が削除されます。破壊されました。したがって、追加や削除が頻繁に行われ、追加や削除される要素の数が多い場合、リストの効率は高くありません。この時点で、それを使用することを検討する必要がありますcollections.deque
。スタックとキューの両方の特性を備えた両端キューであり、複雑な挿入および削除操作をcollections.deque
両端で実行できます。O(1)
list
検索作業も非常に時間がかかります。特定の要素を頻繁に検索する必要がある場合list
、またはこれらの要素に順番に頻繁にアクセスする必要がある場合、オブジェクトの順序を維持してオブジェクト内で二分検索を実行すると、検索の効率が向上しますbisect
。list
もう 1 つの一般的な要件は、最小値または最大値を見つけることです。このとき、heapq
モジュールを使用してlist
ヒープに変換できるため、最小値を取得する時間計算量は になりますO(1)
。
次の Web ページでは、一般的に使用される Python データ構造に対するさまざまな操作の時間計算量が示されています。
時間複雑性 - Python Wikiwiki.python.org
9
参考文献
https://zhuanlan.zhihu.com/p/143052860
デヴィッド・ビーズリーとブライアン・K・ジョーンズ。Python クックブック、第 3 版。オライリー メディア、ISBN: 9781449340377、2013 年。
Zhang Ying および Lai Yonghao、「高品質のコードの作成: Python プログラムを改善するための 91 の提案」、Machinery Industry Press、ISBN: 9787111467045、2014 年。
干货笔记整理
100个爬虫常见问题.pdf ,太全了!
124个Python案例,完整源代码!
PYTHON 3.10中文版官方文档
耗时三个月整理的《Python之路2.0.pdf》开放下载
最经典的编程教材《Think Python》开源中文版.PDF下载
成就感爆棚