Python が素晴らしいのは、学習するのが最も簡単な言語の 1 つであり、驚くほど高速にプロトタイプを作成できるためです。ただし、その利便性のおかげで、作業が大幅に遅くなる可能性がある重大な非効率性が隠れていることがよくあります。注意しないと、作成したきれいなスクリプトがすぐに大きなパフォーマンスのボトルネックになる可能性があります。
基本的なスクリプト作成の段階から進んでいる場合は、一般的な落とし穴について理解しておく必要があります。間違いを犯す前に何をしてはいけないかを知っておくだけで、時間を節約し、頭を悩ませることができます。
メンバーシップ検索にリストを使用する
値がリストにあるかどうかを確認するとき (使用する場合など) if item in my_list)、スクリプトは要素を順番にスキャンします。つまり、ターゲットが見つからないか最後にある場合は、各要素をチェックする必要がある可能性があります。それが線形探索と呼ばれるものであり、 O(n) 手術。データが増大し始めると、要素の検索に必要な時間がリストのサイズに直接比例して増大するため、このプロセスは大幅に遅くなります。
言い換えれば、100 万個の項目のリストで特定の値を探している場合、Python は項目が存在しないと判断する前に、ターゲット値を 100 万個のエントリすべてと比較する必要がある可能性があります。メンバーシップ テストが別のループ内に配置されると (通常は 2 つのデータセットを比較する場合)、これは大幅に遅くなります。
「N+1」クエリの問題
の N+1 クエリの問題は、特にオブジェクト リレーショナル マッパーを使用している場合や外部 API を呼び出している場合に、アプリ開発で遭遇する最も一般的なパフォーマンスの問題であると考えられます。これは、アプリケーションが最初のクエリを実行して項目のリスト (「1」) を取得し、その後、そのリスト内のすべての項目に対して個別のデータベース クエリまたは API 呼び出しをトリガーして、関連するファイルまたはコンテンツ (「N」) を取得するときに発生します。
コード内のロジックは問題ないように見えますが、項目をループして処理しているだけです。規模が拡大するにつれて、パフォーマンスの問題は壊滅的なものになります。これにより、本来は一瞬の操作であるはずの操作が、実際にサーバー スレッドをフリーズさせる可能性のある遅いクロールに変わります。通常、ボトルネックは個々のクエリの実行速度ではなく、データベースへの繰り返しの移動にかかる総コストです。
ループ内の文字列の連結
文字列は不変オブジェクトであるため、文字列を作成すると、その場で実際に変更することはできません。 stringのようなコマンドを実行すると += " data" ループ内で繰り返し実行すると、システムは古いコンテンツと新しいコンテンツの両方を組み合わせるのに十分な大きさのまったく新しいメモリ チャンクを割り当てる必要があります。その後、元の文字列を新しい場所にコピーし、新しい部分を追加し、最後に古いオブジェクトを破棄してクリーンアップを待ちます。
これらを何千回も実行すると、大規模なメモリ チャーンが発生し、データ セットが大きくなるとまったく拡張できない冗長で遅い作業をプロセッサに強制することになります。
ループ内で文字列を直接連結することは避ける必要があります。代わりに、リストと .join() メソッドを使用します。
ファイル全体をメモリに読み取る
テキスト ファイルを読み取ることは Python ではハックとみなされることがありますが、1 つのコマンドを使用してファイル全体をメモリに読み取ろうとしないでください。のようなメソッド f.read() または f.readlines() 小さなテキスト ファイルには便利ですが、データが増大し始めた瞬間に完全な災害に変わります。すぐに MemoryError クラッシュが発生する主な理由は、ギガバイトのデータを一度にロードしようとすることです。
確実な解決策は、ファイルの丸呑みをやめてストリーミングを利用することです。 Python ファイル オブジェクトはイテレータであるため、それらを直接ループできます。を使用する場合は、 for line in file_handler: ブロック内では、インタプリタは一度に 1 つのエントリを読み取って処理し、次の行に移る前にすぐにガベージ コレクタにそのメモリを解放させます。
非効率な入れ子ループ
非効率なネストされたループは目立たないところに隠れることを好み、無害なリスト内包または標準の for ループのように見えるものの中に潜んでいることがよくあります。 1 つのコレクションをループし、そのコレクション内のすべての項目に対して、別のデータ セットを反復処理すると、 O(n) 複雑。データ量が増加すると、計算コストは二次関数的に増加します。
このボトルネックを解決する最も一般的かつ効果的な方法は、反復を開始する前にデータを再構築することです。その内部リストを検索辞書 (ハッシュ マップ) またはセットに変換します。ハッシュ マップを使用すると、インタプリタはキーのハッシュ コードを計算し、それを使用してその値が存在する特定のメモリ バケットに直接ジャンプし、他の要素をスキャンする必要性を完全に回避します。
リソースの開閉を繰り返す
Python がループ内でファイル ハンドルやデータベース接続などのリソースを常に開いたり閉じたりしないようにします。貼り付けるのは簡単です open() Python のクリーンな構文により、呼び出しまたは接続リクエストを for ループに入れることになりますが、これを行うと、単一パスごとにオペレーティング システムが高価なハンドシェイク プロセスを実行することになります。この冗長性によりパフォーマンスが低下します。
ここでの簡単な修正は、リソース取得ロジック全体を反復ブロックから完全に取り出すことです。のようなコンテキスト マネージャーを使用します。 open(...) ステートメントをループの開始前に配置します。リソースを 1 回開くことで、必要なハンドシェイクが 1 回だけ確立されます。その後、ループにジャンプして、すでに開いているハンドルを使用して必要な読み取りまたは書き込み操作をすべて実行できるため、CPU と入出力のオーバーヘッドが大幅に削減されます。
組み込みの最適化関数の無視
リストを並べ替えたり、合計を計算したり、一部のデータをフィルターしたりするためだけに独自のカスタム ロジックを構築したくなる誘惑にかられます。おそらく、それによってより多くの制御が可能になると思われるでしょう。ただし、これらの手動ループに依存することが、Python アプリの実行が遅い主な原因です。ループを効率的なマシンコードに変換するコンパイル言語とは異なり、Python の解釈される性質により、反復ごとにオーバーヘッドが追加されます。これは、生の for ループを実行するたびに、インタープリターの大きなオーバーヘッドが発生することを意味します。純粋な Python でループを作成する場合、インタープリターは常に命令をデコードし、型をチェックし、コレクション内のすべての項目の関数呼び出しを処理する必要があります。
標準ライブラリを信頼して使用してください。 Python の方法でコードを書き始めると、はるかに効率的に作業できるようになります。
理解しておくべき重要なことは、コードの最適化はある種のアルゴリズムの魔法ではないということです。言語が実際に内部でどのように機能するかを尊重し、理解する必要があるだけです。スクリプトが遅いということは、通常、検討されていない小さな決定が積み重なり、パフォーマンスに大きな問題を引き起こすことになります。
前進するには、自分のコードを常に見直してクリーンアップすることが必要です。プログラミングは、そのままでも十分に挑戦的です。これらの一般的な落とし穴がどこにあるのかを学ぶことで、自分のワークフローに不必要なハードルを追加しないようにすることができます。