なぜPythonはコード実行前に「インポート税」を支払うのか
Pythonはコードが実行される前に「インポート税」を支払います。インタプリタの起動時に、モジュールの検索、バイトコードのコンパイルまたは読み込み、そしてモジュールレベルのコード実行という「先行インポート(eager-import)」の連鎖が発生するためです。このプロセスは依存関係グラフ全体を再帰的にたどり、すべてのモジュールとその依存先を読み込みます。
この先行読み込みにはコストがかかります。起動が重いアプリケーション、コマンドラインツール、短命なサーバーレス関数では、このコストが特に顕著です。これらは、インポートしたもののほとんどを使用せずに終了することがあり、実行に寄与しない処理のために時間を費やしていることになります。Pythonのコア開発者が小規模なアプリでベンチマークをとったところ、通常のインポートでは起動に104msかかりました。
「遅延読み込み(Lazy loading)」が解決策となります。即時の実行ではなく、プレースホルダーを提供します。ファイルの検索、バイトコードのコンパイル、モジュールレベルのコード実行といった実際のインポート処理は、コードがそのモジュールを最初に参照した時にのみトリガーされます。これにより、必要な時まで処理を先送りできます。
同じ開発者のベンチマークでは、その影響が示されています。重いインポートを関数内に隠すことで起動時間は46msに短縮され、すべてのインポートを「遅延」させた場合は36msまで短縮されました。これは先行読み込みと比較して約3倍の高速化です。これは実行時間全体の普遍的な改善ではなく、起動に特化した最適化です。
新しい「lazy」キーワードを使用した明示的な遅延インポートは、プラグイン登録のようなモジュールレベルの副作用に依存する既存のコードを壊すことを防ぎます。これにより、予期せぬ結果を招くことなく、的を絞った最適化が可能になります。
高速化の裏にある小さな構文変更
PEP 810は、明示的な「lazy imports」という的を絞った構文ソリューションを導入します。通常のインポートと同じようにファイルの先頭に「lazy import <module>」と記述しますが、インポートされた名前にコードが最初にアクセスするまでモジュールの実行を遅延させます。これにより、依存関係を可視化したまま、起動速度低下の一般的な回避策である「インポート文のコードベースへの散乱」を防ぐことができます。
インポートを関数内に移動させるというお馴染みの回避策は、依存関係を見えにくくし、コードを煩雑にします。ネイティブの「lazy import」はこれを解決します。可読性を犠牲にしたり、インポート場所のリファクタリングを強制したりすることなく、同じ起動メリットを提供します。
Pythonは初回使用時に遅延インポートを解決します。コードがインポートされた名前やモジュールの属性にアクセスすると、インタプリタがそのモジュールを読み込み、コンパイルし、実行します。つまり、アプリケーションは依然としてインポートのコストを支払いますが、それは実行時にその特定の依存関係が実際に必要とされた時のみとなります。
この的を絞ったアプローチは大きな利益をもたらします。Pythonのコア開発者がアプリでベンチマークをとった結果、通常のインポートで104ms、重いインポートを関数内に隠すと46ms、すべてを遅延させると36msでした。これは先行読み込みから完全な遅延読み込みへの移行で約3倍の高速化です。「lazy」キーワードの使用は、「PYTHONLAZYIMPORTS」のようなグローバル設定よりも推奨されます。なぜなら、インポート時にプラグインを登録したり、その他の重要な副作用を実行したりするコードを壊すのを防げるからです。最も安全で大きな効果を得るために、重いインポートのみを対象にすることをお勧めします。
104msの起動が36msに短縮—ただし、これは1つのアプリの例です
Pythonのコア開発者は、小規模なアプリケーションに対する遅延インポートの影響を実証しました。通常の先行インポートを使用したベースラインの起動時間は104msでした。重いインポートを手動で関数内に移動することで、これは46msに短縮されました。明示的な遅延インポートを使用すると、起動時間はさらに36msまで短縮されました。これは先行読み込みのベースラインと比較して2.9倍の高速化を意味します。
これらの数値は可能性を示すものであり、普遍的な保証ではありません。パフォーマンスの向上はアプリケーションによって異なります。自身のプロジェクトでテストを行い、メリットを確認してください。仕様の詳細については、PEP 810 – Explicit lazy imports を参照してください。
Lazy importsは、スタートアップが支配的なワークロードにおいて最も大きな効果を発揮します。これには以下が含まれます:
- コマンドラインインターフェース (CLIs)
- 短時間で終了するローカルスクリプト
- Serverless functionの呼び出し
このようなシナリオでは、初期のインポート負荷が実行時間全体の大きな割合を占めることがよくあります。モジュールの実行を遅延させることで、このオーバーヘッドを直接削減できます。アプリケーションの起動時間を測定し、Lazy importsによって同様のパフォーマンス向上が見込めるか確認してください。
この記事が気に入ったら、毎朝同じようなものをメールで受け取れます。
1日1通 · 2クリックで解除 · サードパーティのトラッキングなし
すべてのインポートをLazyにするのが逆効果になる理由
グローバルなLazy importsは逆効果になる可能性があります。一部のモジュールは、インポート時にプラグインやフック、その他の動作を即座に登録するため、これらを遅延させるとセットアップが期待通りに行われません。これらの副作用に依存するコードは破損します。
PEP 810のような明示的な lazy import 構文を使用すれば、この危険を回避できます。これは、特定の重い依存関係やオプションの依存関係のみを対象として遅延させるものです。これは、すべてのインポートを一括でLazyに変更するグローバルな環境変数やコマンドラインスイッチとは異なります。
MetaのCinderフォーク(Python)は、当初グローバルな遅延読み込みを検討していました。Cinderでの経験は、暗黙的なグローバルLazy importsを提案したPEP 690の策定に活かされましたが、まさにこれらの副作用への懸念から否決されました。
PEP 810の明示的なアプローチはそれらの懸念に対処し、より安全で制御されたメカニズムを提供します。lazy import を採用する前に、必ずPythonのバージョンと機能の具体的なステータスを確認してください。これは大幅な起動時間の短縮をもたらしますが、慎重かつターゲットを絞った適用が必要です。
よくある質問
PythonにおけるLazy importsとは何ですか?
Lazy importsとは、インポートされたモジュールの読み込みと実行を、コードで実際に使用されるまで遅延させる手法です。
Lazy importsでPythonの起動はどれくらい速くなりますか?
ある小規模アプリのベンチマークでは、起動時間が104msから36msに短縮され、約2.9倍高速化しました。結果はアプリケーションによって異なります。
Lazy importはどのように記述しますか?
明示的な形式である lazy import module を使用して、そのインポートが必要になるまで遅延させます。
Lazy importsによってPythonアプリケーションが破損することはありますか?
はい。プラグインを登録したり、インポート時に副作用を伴うモジュールを遅延させると、動作が変わる可能性があります。明示的かつターゲットを絞った使用により、そのリスクを軽減できます。

