HN 日本語サマリー

← 一覧へ戻る
プログラミング

Pythonを遅延ロード可能にするための取り組み

Working to Make Python Lazy (iscinumpy.dev)

31 pointsby js25 コメント

要約

Python 3.15で導入される「遅延インポート」機能により、CLIアプリケーションや多数のインポートを持つ大規模コードベースのパフォーマンスが向上します。この機能は、モジュールが実際に使用されるまでインポート処理を遅延させることで、不要なインポートによるオーバーヘッドを削減します。記事では、この機能の仕組みと、それを容易にするためのツール「flake8-lazy」の紹介、そして開発におけるAIの活用経験について解説しています。

全文翻訳

Python 3.15a7は、すべての主要プラットフォームでわずかuv python install 3.15離れたところで、遅延インポートをサポートします。PEP 810で提案されたこのエキサイティングな機能は、CLIアプリケーション(特に--helpのようなフラグを使用する場合)を高速化し、常に使用されない多数のインポートを持つ大規模なコードを高速化する可能性があります。以前の失敗した試みとは異なり、これにはライブラリ側での作業が必要です。私はそれを容易にするためのヘルパーツールを開発しました。遅延インポートとは何か、そして私のツールをどのように使用するかを説明したいと思います。この投稿の後半では、この種のタスクでAIをどのように活用したかについての私の経験をカバーします。 TL;DR: uvx flake8-lazy --apply=list を実行して、Python 3.15でコードを魔法のように高速化しましょう! 遅延インポートとは何か? 標準的なPython argparse CLIを持つ、次のようなファイルがあると想像してください。 ```python import argparse import numpy def main(): parser = argparse.ArgumentParser() parser.add_argument("--foo", action="store_true") args = parser.parse_args() if args.foo: print(numpy.array([1, 2, 3])) ``` このコードを--help付きで実行するとどうなるでしょうか? numpyライブラリは、実際には使用されていないにもかかわらずインポートされます。最新のuvツールを使用している場合、uvは明示的に要求されない限りバイトコードを事前コンパイルしないため、これはさらに悪化する可能性があります。これによりインストールは高速になりますが、初回インポートは遅くなります。 上記の例は1つにすぎません。次のような一般的なパターンでも発生する可能性があります。 ```python # __init__.py from . import a from . import b __all__ = ["a", "b"] ``` このアイデアは、ユーザーが `import lib.a` ではなく `import lib` だけで `lib.a.stuff` を使用できるようにすることですが、それらがすべてのインポートを使用しない場合でも、インポートのコストを支払うことになります。richのような一部のライブラリはこれを回避することに注意しており、ユーザーに明示的なインポートを求めていますが、多くの古いライブラリはこれを行っていました。 また、複数の機能(サブコマンドを持つCLIライブラリなど)を実行できるライブラリもありますが、すべてのサブコマンドで依存関係が必要なわけではありません。 Python 3.15の遅延インポートの使用方法 最初の例を取りましょう。Python 3.15では、次のように記述できるようになりました。 ```python lazy import argparse lazy import numpy def main(): parser = argparse.ArgumentParser() parser.add_argument("--foo", action="store_true") args = parser.parse_args() if args.foo: print(numpy.array([1, 2, 3])) ``` これで、両方のインポートが「遅延」になります。つまり、インポート時に何も起こりません。それらはインストールされていない可能性さえあります。しかし、オブジェクトを初めて使用しようとすると、それは実際のインポートされたオブジェクトになります。したがって、--helpを実行しても、numpyは決してアクセスされず、インポートもされません。 後方互換性のある構文もあります。 ```python __lazy_modules__ = ["argparse", "numpy"] import argparse import numpy ``` これは古いPythonでも機能します(単に遅延ではないだけです)。動的にリストを生成または操作することもできます。Ruffのようなリンターは、これを通しコード違反を引き起こさずにインポートの上に配置できるように更新されています。 Python 3.15a7には、すべてのインポートを遅延として扱うためのフラグと変数(-X lazy_imports=all および PYTHON_LAZY_IMPORTS=all)があることに言及しておきます(normalおよびnoneもあります)。これは主にテスト用です。 Python 3.15a7では、noneは__lazy_modules__を無効にしません。構文バージョンのみを無効にします!修正される予定です。 現在、遅延インポートを無効にすると構文キーワードが無効になるため、循環インポート、型チェックなどには使用できません。個人的には、noneは後方互換性の__lazy_modules__バリエーションのみを無効にすべきだと思います。遅延インポートは保証された遅延インポートであるべきです。このオプションの存在は、現在標準ライブラリでの使用を妨げています。 なぜ遅延しないのか? すべてのモジュールを遅延としてマークすべきではないでしょうか? そうする必要はありません。インポート時に副作用があるモジュールがいくつかあります。それらの副作用がインポートサイトで発生する必要がある場合、それらは遅延できません。たとえば、このパターンは遅延できません。 ```python try: import numpy except ModuleNotFoundError: ... ``` ここでのエラーは、numpyの何かを最初に使い始めたときに発生します。半遅延の代替案があります。 ```python import importlib.util if importlib.util.find_spec("numpy") is None: ... # numpy が欠落している場合の処理 lazy import numpy ``` これは、何も行わないよりもわずかにコストがかかります(だからこそ遅延インポートはそれを行いません)。パッケージをインポートしてサブパッケージに到達します(a.b は a をインポートします)。また、仕様を見つけるだけで発生しないインポートエラーもあります(上記の例では、numpy._core が欠落または破損している可能性があります。これはまれですが)。それにもかかわらず、これはパッケージがインストールされているかどうかを確認するためのかなり良い方法です。 遅延が不要なもう1つのケースは、トップレベルで何かを使用する場合です。たとえば、 ```python lazy import re REGEX = re.compile(...) # ここでは遅延ではない ``` ここでは、ファイルがそれをインポートせずに処理できないため、遅延インポートは不要です。キャッシュを使用してこれを回避できます。 ```python # 3.15+ import functools lazy import re @functools.cache def regex() -> re.Pattern: return re.compile(...) # Older from __future__ import annotations __lazy_modules__ = ["re"] import functools import re @functools.cache def regex() -> re.Pattern: return re.compile(...) ``` この場合、注釈のためにこのコードが機能するために `from __future__ import annotations` が必要ないことに注意してください。Python 3.14では注釈がデフォルトで遅延になったため、`re` モジュールはロードされません。 これらの種類のインポートを遅延させることはできますが、理由もなくインポートエラーを移動しているだけなので、そうしない方が少し良いです。 ファイル内のすべてのインポートを遅延させたい場合は、次のように行うことができます。 ```python class AllLazy: @staticmethod def __contains__(_: str) -> bool: return True __lazy_modules__ = AllLazy() ``` これは単に、完全なモジュール名に対してテストで使用され、独自のオブジェクトをここに入れることができます。(以下の静的ツールはまだこれを見つけません。) 支援ツール ライブラリは理想的にはこれらの__lazy_modules__を追加し始めるべきですが、すべてをそれに含めるよりも少し複雑です。そこで、何を追加すべきか、そしてそれを整理しておくのに役立つツール、flake8-lazyを作成しました。これは、AIツールを開発に大量に使用した最初のライブラリです(plumbumのメンテナンスを支援するために使い始めましたが、ゼロからではありません)。そのため、最後にその経験について(非常にうまくいきました)セクションを設けます。過去にflake8-errmsgを開発したことがあるので、これは私の最初のflake8プラグインではありません。そのプロジェクトと同様に、組み込みのスタンドアロンランナーもあります。3.15ライフサイクルの初期には、それが主な使用方法になると予想しています。 使用方法: ```bash # flake8スタイルのエラーを表示 uvx flake8-lazy <filenames> # 追加する必要がある行を表示 uvx flake8-lazy --format=lazy-modules # 追加するだけ! uvx flake8-lazy --apply=list <filenames> # flake8スタイルのエラーを表示 pipx run flake8-lazy <filenames> # 追加する必要がある行を表示 pipx run flake8-lazy --format=lazy-modules # 追加するだけ! pipx run flake8-lazy --apply=list <filenames> ``` これにより、エラーが報告されます(noqaはシンプルなランナーでは機能しません)。 現在実装されているエラー(0.6.0): コード 1xx: 遅延宣言の欠落 LZY101 stdlibモジュールは__lazy_modules__にリストされている必要があります LZY102 サードパーティまたはローカルモジュールは__lazy_modules__にリストされている必要があります これらはトップレベルで使用されていないものを検索し、__lazy_modules__に追加することを提案します(遅延構文も機能します)。現在、注釈がインポートをトリガーしないと仮定しています(flake8は、ターゲットとする最小Pythonバージョンを知らないため、Ruffとは異なり、3.14以上かどうかを判断できません)。 コード 2xx: __lazy_modules__の検証 LZY201 __lazy_modules__はソートされていません LZY202 __lazy_modules__にリストされているモジュールは決してインポートされません LZY203 __lazy_modules__にリストされているモジュールは重複しています LZY204 __lazy_modules__は、名前が付けられたモジュールをインポートした後に割り当てられます LZY205 __lazy_modules__にリストされているモジュールは絶対名である必要があります これらは、__lazy_modules__に関する一般的な問題を検索します。 コード 3xx: ネイティブ遅延キーワード(Python 3.15+) LZY301 ImportErrorを抑制する中の遅延インポートは誤解を招きます LZY302 遅延キーワードと__lazy_modules__の両方によって宣言されたモジュール LZY303 モジュール