脱ぐ!クールな操作をスピードアップするための 8 つの Python ヒント

記事のソース: 新人のための 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.sqrtcomputeSqrtfunction には別のもの.、つまり呼び出し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 ais がFalse計算なしで直接返されることbif a or bそのようなステートメントのawhenTrue is が計算なしで直接返されることを意味しますbしたがって、実行時間を節約するには、forステートメントのorTrue確率が高い変数を最初に書き込み、後回しにする必要があります。orand

# 推荐写法,代码耗时: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.jitnumbaPython 関数をマシンコードに 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 の組み込みデータ構造 ( strtuple、 、などlist)はすべて C で最下位レベルで実装されており、非常に高速ですが、新しいデータ構造を自分で実装してパフォーマンスの面で組み込みのset速度を達成することはほとんど不可能です。dict

listC++ と同様にstd::vector、これは動的配列です。一定量のメモリ空間が事前に割り当てられます。事前に割り当てられたメモリ空間が使い果たされ、そこに要素が追加されると、より大きなメモリ空間が適用され、元の要素がすべてそこにコピーされ、その後、以前のメモリ、新しい要素を挿入する前のスペース。要素を削除する場合も同様で、使用されているメモリ空間が事前に割り当てられたメモリ空間の半分未満の場合、追加の小さなメモリが適用され、要素のコピーが作成され、元の大きなメモリ空間が削除されます。破壊されました。したがって、追加や削除が頻繁に行われ、追加や削除される要素の数が多い場合、リストの効率は高くありません。この時点で、それを使用することを検討する必要がありますcollections.dequeスタックとキューの両方の特性を備えた両端キューであり、複雑な挿入および削除操作をcollections.deque両端で実行できます。O(1)

list検索作業も非常に時間がかかります。特定の要素を頻繁に検索する必要がある場合list、またはこれらの要素に順番に頻繁にアクセスする必要がある場合、オブジェクトの順序を維持してオブジェクト内で二分検索を実行すると、検索の効率が向上しますbisectlist

もう 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下载
成就感爆棚

おすすめ

転載: blog.csdn.net/wuShiJingZuo/article/details/133109175