世界を動かす技術を、日本語で。

PEP 810 – 明示的遅延インポート

概要

  • Python 3.15 で導入予定の lazy import 構文提案
  • lazy import は、指定したモジュールの 読み込み・実行を初回アクセス時まで遅延
  • 起動時間短縮・メモリ節約 など、特にCLIツールや大規模アプリで有効
  • 従来のimport文との 後方互換性を完全維持
  • 明示的・局所的・安全に インクリメンタル導入 可能

PEP: 明示的な lazy import 構文の導入

  • lazy import 構文の新設による、遅延インポート機能の明文化
    • 例: lazy import json
    • 例: lazy from json import dumps
  • 通常のimport文 は即時インポート、 lazy import は初回利用時まで遅延
  • 不要な依存モジュールの即時ロード回避 による、アプリ起動の高速化
  • メモリ使用量の削減 と、実際に必要なコードパスのみ依存解決
  • コマンドラインツール・テストスイート・大規模依存アプリ での有用性
  • 既存のimport文は影響なし、lazy指定時のみ遅延動作
  • if TYPE_CHECKING: など型チェック用インポートの最適化
  • 局所的・明示的・逐次的な導入 を重視した設計
  • 依存グラフの可視性維持、import分散による可読性低下を回避
  • LazyLoader やサードパーティ実装との差別化
  • lazy_modules による、旧バージョンとの互換サポート
  • dictの改変やグローバルフラグ方式は不採用、安全性と最適化重視

動機と利点

  • モジュール先頭でのimport集中 が従来の慣例
    • これにより 依存関係の明示性一度きりの評価 を実現
  • 即時インポートによる無駄な依存ロード が、起動遅延・メモリ増大の原因
  • 関数内import による手動遅延手法は、保守性・依存追跡性に課題
  • 標準ライブラリ分析 でも、約17%が遅延目的で関数内importを採用
  • lazy import構文 により、 依存の一元管理・明示的遅延 を両立
  • CLIツールの起動時間50-70%短縮、メモリ30-40%削減の実績
  • 型アノテーション用import のランタイム負荷ゼロ化
  • 大規模アプリの未使用サブシステムのメモリ節約

設計上の根拠と意思決定

  • 明示的なlazyキーワード の導入で意図を明確化
  • import文単位での局所的適用、依存先へのカスケードはなし
  • 辞書(dict)の改変回避、プロキシオブジェクト方式で安全性確保
  • 通常importとの区別を容易化、後方互換性の維持
  • lazy_modules による段階的導入サポート
  • インクリメンタルな導入 で既存コードベースへの影響最小化
  • グローバルスイッチ による一括適用も選択肢として用意

他の設計判断

  • 遅延のスコープは完全にローカル (import文ごと)
  • グローバルなlazy import制御 も可能(大規模アプリ・テスト環境向け)
  • 辞書(dict)のサブクラス化やグローバルフラグ案は不採用
    • Pythonオブジェクトの根幹であるdictの最適化・安全性重視
  • プロキシ方式 で初回アクセス時に通常importへ置換
  • CLIツール開発者などが段階的に導入できる構成

このPEPは、 Pythonのimport機構に明示的な遅延インポート機能 を加えることで、 起動時間・メモリ効率・依存管理性 の向上を目指すものです。 既存コードとの互換性・導入のしやすさ・安全性 を重視した設計となっています。

Hackerたちの意見

いい感じの機能だね。説明もシンプルで、実際の使い方もあって、スコープも明確(グローバルのみ、かなりシンプルなキーワード)。気に入った!

うん、これはかなりクリーンなPEPだと思うよ。特にユーザースペースの視点から見てね。伝統的な文法の議論が終わった後、どうなるか楽しみだな。

PEP-690が却下された理由から学んでくれたらいいな。うちのコードベースのためにこれを作るのにかなりの時間を費やしたけど、うまくいった試しがないんだよね。

同意するよ。彼らは本当に宿題をやったし、エッジケースをリストアップして、実用的な妥協をし、やりすぎないように選んで、何度も何度も再構築して、実際の経験と比較している。特に、Pythonのように多様なコミュニティを持つ言語のバックボーン(インポートシステム)に手を加えるのは超危険な手術だから、本当に美しい仕事だと思う。感心してるよ。

誰か興味があれば、結構使いやすいレイジーインポートの仕組みをコンテキストマネージャーの形で実装したよ(auto_proxy_import/init)。これを結構使ってる。文法的には、特に変更のないインポート文をwithブロックでラップするだけだから、ツールもそのまま動くし、簡単に無効化したりデバッグ用に早めにインポートさせたりできる。主にフレームのf_builtinsをcextで入れ替えることで動いてるけど(importlibのフックよりもパワーが必要だから)、スレッドセーフな純粋なPython版の試みもしてるし、超バカなグローバルフック版もある。最初は懐疑的だったけど、今では大部分のコードをこれに移行したよ。意外と問題は少なくて(正直、いくつかのモジュールでインポート時の登録を忘れるくらい)、速度の向上は中毒性があるね。

明示的にオンオフできるから好きだな。Dockerベースのアプリでは、インポートの完全性を確認したいんだ。できればビルドとテストの時にね。実際、ほとんどの場合はレイジーローディングを完全に無効にすると思う。でも、もっと早く読み込めるCLIツールがあったら嬉しいな。ただ、Pythonには、例えばpandasがExcelライブラリをインストールしてないとエラーを出すパターンがあるから、それはそれでいいんだけど。将来的には、メンテナーたちはスタートアップ時間に悪影響を与えないからって、使わないライブラリをたくさん含める選択をするのかな?(pandasがデフォルトで3-4個のExcelパーサーを含めるみたいに、呼び出された時だけ読み込まれるから)。UXはずっと良くなるけど、レイジーローディングをオフにすると、コードの読み込みが遅くなるんだよね。

レイジーインポートは以前にも提案されたことがあって、最近では2022年に却下されたよね。もし記憶が正しければ、レイジーインポートはCinder、MetaのCPythonバージョンでサポートされていて、PEPはCinderに関わっていた人たちによって推進されたんだ。前回は、これがオプトインかオプトアウトか、どのレベルでやるべきか、CPython自体のビルドフラグにするべきかなどの質問が多く出たよ。リンク先の投稿によると、ステアリングカウンシルは、2つの異なる「モード」のインポートを持つことによる複雑さから最終的に却下したみたい。今回の提案が成功することを願ってる。この機能を使いたいな。

うーん。バージョンインポートをサポートしてほしいな。例えば、import torch==2.6.0+cu124import numpy>=1.2.6 みたいに。Pythonライブラリの複数のバージョンを同時にインストールできるようにして、condaやvirtualenv、docker、bazelのごちゃごちゃを終わらせてほしい。

特にオプトインで、さまざまな粒度のレベルがあって、グローバルなオフスイッチもあるからね。制約を考慮した上で、よく構築された仕様だと思う。

これ大好き。私のCLIツール(https://llm.datasette.io/)はプラグインをサポートしてるんだけど、「llm --help」みたいなコマンドでも起動がすごく遅いって文句があったんだ。人気のプラグインがベースレベルでpytorchをインポートすることがあって、全体の起動が重いインポートでブロックされてたみたい。プラグインの作者向けドキュメントに、関数内でのレイジーローディングを提案するノートを追加したけど(https://llm.datasette.io/en/stable/plugins/advanced-model-pl...)、これがPythonのコア機能としてあったら本当にいいな。

これを今日から自分のツールで実装できるよ: https://news.ycombinator.com/item?id=45467489 これはプロセス全体にグローバルだから、例えばNumpyをこの方法でレイジーにインポートすると、すべてのサブモジュールのインポートもレイジーになる。つまり、Numpyの大部分は必要ない場合、全くインポートされないかもしれないけど、個々のモジュールのインポートのための待機時間がランタイム全体で予測不可能に分散されるかもしれない。編集:さらに実験した結果、ソースが import foo.bar.baz のようなことをすると、foofoo.bar はまだ早めにロードされて、foo.bar.baz 自体だけが遅延されるみたい。これがPEPが「ほとんど」と言っていたことの一部かもしれない。でも、私の実装を改善してそれを修正することもできるかもしれない。

コマンドラインを解析して、インポートをせずに「--help」みたいなことをする。必要だとわかった時だけインポートを行うべきだし、簡単なコマンドラインオプションが処理された後にまだやることがあるときだけインポートすればいい。

明示的なトップレベルのインポートを好むのは、プログラムが始まった瞬間に依存関係の問題を明らかにするからだよね。特定のコードパスが実行されるまで数時間や数日後になるのは避けたいから。

「私たち」って誰?

PEPをまだちゃんと理解してないけど、依存関係の検証のためにコマンドラインフラグか外部ツールがあるといいな。タイプアノテーション用の外部ツールみたいに。

そうだね。なんでかわからないけど、Claudeが生成するコードにはローカル依存関係が多い気がする。グローバルに宣言されたインポート文じゃなくて。こんなパターンは推奨しない方がいいよ。 - モジュールの依存関係が見えにくくなるし、 - 後で循環依存関係を引き起こすリスクが高まるから。

反論として、すべてのインポートを自動的に遅延させると、短いタスクのためにpipが劇的に速くなる。

$ time pip install --disable-pip-version-check
ERROR: You must give at least one requirement to install (see "pip help install")
real    0m0.399s
user    0m0.360s
sys     0m0.041s

ほとんどの時間は、最終的に役に立たないベンダーコードのインポート(と後のアンロード)に費やされている。私のテスト(ラッパースクリプトをハッキングして診断情報を出力させた)から見ると、合計で約500モジュールがインポートされている(デフォルトのPythonプロセスのベースラインの上に)、Requestsやその依存関係に関連するモジュールがほぼ100個含まれているのに、このコマンドにはウェブリクエストは必要なかった。

標準ライブラリはLazyLoaderクラスを提供していて、いくつかの非効率な問題を解決するために使える。モジュールレベルでのインポートをインラインインポートのように機能させることができる。この種のPythonインポートの内部を使うのは、かなり分かりにくい。私が見つけたStack OverflowのQ&A(https://stackoverflow.com/questions/42703908/)も、特に見栄えのいいUXにはなってない。だから、特別な構文なしで自動的にすべてのインポートをレイジーにするための既存のPythonでの概念実証をここに示すよ。

import sys
import threading  # python 3.13では必要、REPLでの理由から
from importlib.util import LazyLoader  # これは早めにインポートしなきゃいけない!
class LazyPathFinder(sys.meta_path[-1]):
    # @classmethod
    def find_spec(cls, fullname, path=None, target=None):
        base = super().find_spec(fullname, path, target)
        base.loader = LazyLoader(base.loader)
        return base
sys.meta_path[-1] = LazyPathFinder

「メタパスファインダー」を自分たちのラッパーに置き換えた。結果として得られたスペックに付随する「ローダー」は、importlib.util.LazyLoaderのインスタンスに置き換えられる。インポート文が実際にモジュールをインポートするとき、その名前は普通のモジュールではなくインスタンスにバインドされる。このインスタンスの属性にアクセスしようとすると、通常のモジュールロード手続きがトリガーされる — それはグローバル名も置き換える。今、こうできる:

import this  # 何も表示されない
print(type(this))  # rot13 = this.s  # モジュールがロードされて、Zenが表示される
print(type(this))

「とはいえ、ここでPEPが「ほとんど」と言っている意味はわからないけど。」

嫌いではないけど、好きでもない。ほとんどすべてのインポートの前に lazy を書くようになる気がする。例外は、実際に早めにインポートが必要な場合だけ。そうなるとPythonコードが視覚的にうるさくなるし、デフォルトを変える計画もないから、そのうるささは永遠に残る。モジュールがレイジーロードされることを選ぶシステムの方が良かったな。インポート側に余分な構文がない方が簡単になるし、大きなライブラリだけがレイジーさを気にすればよくなる。公平に言うと、そんなデザインでは、インタープリターがファイルシステム上でインポートを早めに探す必要があるから、レイジーロードすべきかどうかを判断するために。おそらく他にも考えていないデメリットがあるかもしれない。

タイプやセイウチ、asyncio、データクラスなどについて聞いたけど、実際には起こらなかったね。人々が何かを必要としていないなら(多くの人はそれが存在することや何をするかも知らない)、使うことはないと思う。実際、コミュニティの半分はほとんどPython 2.4の機能を現代風に使っているだけで、それがこの言語の魅力の一つなんだ。生産的でいるためにたくさんのものは必要ないし、もっと欲しいならオプションで手を伸ばせばいい。過去20年間うまく機能してきたし、これからもきっと大丈夫だろうね。

モジュール側でこれを考えるのは意味がないと思う。呼び出し元がモジュールを遅延読み込みできるかどうかの情報を持っているからね。インポートされるモジュールが決めることは本当に何もない。どのモジュールも遅延読み込みできるし、副作用があっても、呼び出し元はそれを遅らせたいかもしれない。

pyproject.tomlが正規表現を使って遅延読み込みを指定できるように強化されてほしいな。

すべてのモジュール読み込みを遅延させるコマンドラインフラグをPythonに渡せるなら、喜んで受け入れるよ。スクリプトや非常にシンプルなものを書いている場合を除いて、モジュールが読み込まれるときに副作用が発生するのは避けるべきだと思う。

みんなが特に気にせずに怠惰なインポートを好むようになったら、怠惰がデフォルトの動作で、イーガーが足りないキーワードってことになるよね。Pythonがこのパラダイムを見直すのはこれが初めてじゃないし。v2でリストをイーガーに生成してた多くの構文が、v3ではほとんど問題なくジェネレーターに変わったからね。

PEPがあまり触れていない一つのことが、すごくイライラするんだけど、多くのPythonリンターはインポートをファイルの先頭に置かないと文句を言うから、遅延インポートを実装する最も明白な方法を取るとリント警告が出るんだ。これはパフォーマンスだけでなく、他にも問題がある。場合によっては、先頭でインポートすると失敗することもあるからね。例えば、特定のプラットフォームでのみ必要なライブラリがある場合、そのプラットフォームで実行されているときだけ必要なんだ。

[遅延]

[遅延]