Python 3.15のlazy importと内包表記アンパッキングをコードで理解する

3 minutes reading View : 1
アバター画像
Aiko Yamamoto
IT - 19 5月 2026

Python 3.15の新機能として追加された「モジュールの読み込みを必要な時点まで遅延するlazy import」と、二重ループの構造を取る内包表記をより直感的に書けるようになる「内包表記でのアンパッキング」について、実際にコードを書いて調べてみました。

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

前回はPython 3.15の新機能を概観しましたが、今回はそのうちのlazy importと内包表記でのアンパッキングの2つについて深掘りしてみます。簡単に書けるかな?と思っていたら、意外に長くなっちゃいました(笑)。

Python 3.15ではlazy import(PEP 810)が導入されます。これは「モジュールをインポートする際に、『lazy』とマークが付いていたら、そのモジュールを実際に使うまではロード(と関連する初期化処理)が行われないようになる」というものです。

大規模なPythonアプリは多くのパッケージやモジュールに依存していて、それらをアプリの起動時に読み込むことがよくあります。これがアプリの起動時間に影響を与えます。

例えば、Python製のコマンドラインツールでヘルプを表示したいだけなのに、「python -m … –help」のようにして起動すると、依存するモジュールを読み込むのにそれなりの時間がかかった上で、ヘルプ情報をコンソールに表示して終わり。ヘルプの内容を確認できたので、今度はそのツールにコマンドを指定して実行すると、また依存するモジュールの読み込みが行われるなんてことが考えられます。

実際には使われないモジュールでもプログラムの起動時に全て読み込むのではなく、必要なモジュールをオンデマンドで読み込むようにしましょうというのが、lazy importが目指すところだといえるでしょう。

import文をプログラムやモジュールの先頭に置くのではなく、関数内に置いたり、importlibモジュールを使ったりすることで、オンデマンドにインポートを置くことも可能ですが、モジュール管理という意味では、必要とするものがプログラムのあちらこちらに分散して記述されることになり、あまりよろしくはないといえます。これに対して、lazy importなら「必要なモジュールはプログラムやモジュールの先頭に置きつつも、そのロードは本当に必要なときまで遅延できる」というメリットがあるというわけです。

書き方は簡単で、import文またはfrom import文の先頭に「lazy」と付けるだけです。以下に例を示します(jsonモジュールとpathlibモジュールに特に意味はありません。「What’s new in Python 3.15」で例に使われていたからくらいの理由です)。

といっても、これではlazy importできているのかどうかがよく分かりません。というわけで、次の2つの.pyファイルを作ってみました。

# moda.py def func_a(): print(‘func a’) print(‘func_a defined’)

# modb.py def func_b(): print(‘func b’) print(‘func_b defined’)

moda.pyはfunc_a関数を定義して最後に「func_a defined」と表示します。modb.pyは同様に、func_b関数を定義して最後に「func_b defined」と表示します。通常のimportとlazy importでこれらの挙動がどう変わるかを確認してみましょう。

以下は、Python 3.14で「import moda」「from modb import func_b」を実行したところです。

これはいつも通りのインポートの挙動です。では、Python 3.15でlazy importを試してみます。

「lazy import moda」「lazy from modb import func_b」を実行した時点では、コンソールにメッセージが表示されていません。これがモジュールのロードが遅延されていることを示すものです。そして、「moda.func_a()」「func_b()」と2つのモジュールを使った時点でそれらがロードされ(トップレベルのコードも実行されて)コンソールに出力が行われ、さらにfunc_a関数とfunc_b関数が呼び出されて関数からのメッセージも表示されたというわけです。

なお、lazy importをグローバルに指示する(「import moda」「from modb import func_b」と書いた場合でもlazy importが行われるようにする)ことも可能です。これにはPython処理系の起動時に「-X lazy_imports=all」とオプションを指定するか、環境変数PYTHON_LAZY_IMPORTSの値をallにして、Python処理系を起動します。

なお、「-X lazy_imports」オプションと環境変数PYTHON_LAZY_IMPORTSには以下の3つを指定できます。

グローバルな指示でallを指定すると、「lazy」の有無に関わらず、全てのモジュールのインポートが遅延されます。これは「Python 3.14までのコードをそのままPython 3.15で実行するけれど、モジュールのロードは遅延したい」という場合に大いに役立ってくれそうですね。

「-X lazy_imports=…」および環境変数PYTHON_LAZY_IMPORTSで行った設定はsysモジュールのset_lazy_imports関数で上書きできます。また、sys.get_lazy_imports関数で現在の設定を取得可能です。

この例では、「-X lazy_imports=all」付きでREPLを起動しています。そのため、sys.get_lazy_imports関数は’all’を返します。そこでsys.set_lazy_imports(‘normal’)を呼び出しているので、lazy付きのimport文は遅延され、そうでなければ遅延されないようになります。その後の「import moda」はモジュールのロードがすぐに行われて、「lazy from modb import func_b」はfunc_b関数を呼び出すまでロードが遅延されていることを確認してください。

なお、インポートを行う際にモジュール単位でロードを遅延するかどうかを指定することも可能です。これにはsys.set_lazy_imports_filter関数に、モジュールのロードを遅延するかどうかを判定するフィルター関数を渡します。

これはモジュールをインポートした時点で、何らかの処理が開始される(例えば、ログや統計情報の記録など)ことを前提とした「副作用のある」モジュールも世には存在しているからです。上で紹介したグローバル指示で全てのモジュールのロードを遅延している際に、そうしたモジュールのロードまで遅延してしまうと、想定とは異なる振る舞いが発生してしまいます(例えば、統計情報を記録しているはずが、モジュールの読み込みが遅延されたので、何も記録されておらず、統計情報の取得をしようとした時点でやっとモジュールがロードされたら困りますよね)。そうしたことを避けるために、モジュール単位でのロードの遅延の可否を指定できるようになっているものと思われます。

def myfilter(importing, imported, fromlist): print(f'{importing}, {imported}, {fromlist}’) if imported == ‘moda’: return True for name in fromlist: if name == ‘func_b’: return False return True import sys sys.set_lazy_imports_filter(myfilter)

フィルター関数にはインポートしているモジュール、インポートされようとしているモジュール、from import文で指定された名前を要素とするタプルが渡されるので、これらの情報を基にそのモジュールのロードを遅延するかどうかを判断し、遅延するならTrueを、遅延せずにすぐロードするならFalseを返すようにします。

この例では、インポートされようとしているモジュールが’moda’なら遅延、「from import」で’func_b’をインポートしようとしているのであれば、すぐにロードするようになっています(2つの条件判定と無縁なものは遅延)。そして、この関数をsys.set_lazy_imports_filter関数に渡すことで、インポート時にフィルター関数が呼び出されるようになります。

筆者が試したところでは、「-X lazy_imports=all」などでlazy importがデフォルトで有効でないと「lazy」なしのimport文を実行すると、フィルター関数が呼び出されないようになっていました。この辺の挙動には少し注意が必要かもしれません。

また、先ほどは「-X lazy_imports=all」を指定するか、環境変数PYTHON_LAZY_IMPORTSの値をallにすることでデフォルトでモジュールのロードを遅延するようにできるといいましたが、似たことは__lazy_modules__にロードを遅延したモジュールを列挙する方法もあります。

こちらの方法でも、__lazy_modules__に対象のモジュールを列挙する変更を加える必要はありますが、従来のコードをPython 3.15で実行する際にlazy importを行えるようになります。

内包表記でのアンパッキングについては「Python 3.15 β1がリリースされたので新機能や変更点をまとめてみた」で以下のような例を示しました。

mylist = [[0, 1], [2, 3]] # for文で書くと flatten = [] for items in mylist: for item in items: flatten.append(item) print(flatten) # [0, 1, 2, 3] # 内包表記を使うと flatten = [item for items in mylist for item in items] print(flatten) # [0, 1, 2, 3]

このコードがPython 3.15では次のように書けるようになりました。

# unpacking comprehensionあり flatten = [*items for items in mylist] print(flatten) # [0, 1, 2, 3]

二重ループや内包表記のネストを使っていたところが、内包表記でのアンパッキングを使うことで、より直感的な記述が可能になったということです。内包表記なので、リストでなくて集合や辞書でもかまいません。あるいはジェネレーター式でもよいでしょう。以下に例を示します。

mylists = [[0, 1], [2, 3]] mysets = [{0, 1}, {1, 2}] mydicts = [{‘key0’: ‘value0’, ‘key1’: ‘value1’}, {‘key1’: ‘value2’}] result_lists = [*item for item in mylists] # リスト内包表記でのアンパッキング result_set = {*item for item in mylists} # 集合内包表記でのアンパッキング result_dict = {**d for d in mydicts} # 辞書内包表記でのアンパッキング result_gen = (*it for it in mylists) # ジェネレーター式でのアンパッキング

アンパッキングには多くの読者にはもうおなじみの「*」や「**」を使います。内包表記でのアンパッキングを使うことで、コンテナオブジェクトを要素とするリストのフラット化(平滑化)をより直感的に行えるというわけです。

「ちょっと待って!」となる人もいるかもしれません。というのは、従来の内包表記はforループに書き下せたからです。以下はリスト内包表記の例です。

mylist = [0, 1, 2, 3] # 内包表記 result = [item * 2 for item in mylist] # for文 result = [] for item in mylist: result.append(item * 2)

では、内包表記でのアンパッキングを使ったコードはどのようにして書き下せるのでしょうか。といっても、リスト内包表記ではappendメソッドではなく、extendメソッドを使うだけです。extendメソッドって、引数として与えた反復可能オブジェクトを展開して、リストの要素として追加するものでしたよね。

mylists = [[0, 1], [2, 3]] # 内包表記でのアンパッキング result0 = [*item for item in mylists] print(result0) # [0, 1, 2, 3] # 対応するfor文 result1 = [] for item in mylists: result1.extend(item) print(result1) # [0, 1, 2, 3] print(result0 == result1) # True

じゃあ、集合と辞書はどうなるでしょう。その場合はupdateメソッドによる更新だと考えればOKです。以下に例を示します。2つの集合は要素が被っていること、2つの辞書ではキーが重複している点に注意してください。

mysets = [{0, 1}, {1, 2}] mydicts = [{‘key0’: ‘value0’, ‘key1’: ‘value1’}, {‘key1’: ‘value2’}] # 内包表記でのアンパッキング result_set0 = {*s for s in mysets} print(result_set0) # {0, 1, 2} result_dict0 = {**d for d in mydicts} print(result_dict0) # {‘key0’: ‘value0’, ‘key1’: ‘value2’} # 対応するfor文 result_set1 = set() for s in mysets: result_set1.update(s) result_dict1 = {} for d in mydicts: result_dict1.update(d) print(result_set0 == result_set1) # True print(result_dict0 == result_dict1) # True

ジェネレーター式ではyield from文で要素をyieldするのに相当します(コードは省略)。

集合についてはその特性から重複する要素はなくなること、辞書については同じキーがあった場合には後の要素で値が上書きされることには注意してください(といっても、集合と辞書の通常の振る舞いではあります)。

今回はフラット化(平滑化)の例しか取り上げられていませんが、他にもいろいろと可能性がありそうです。機会があれば、そうした活用法についてもご紹介することにしましょう。今回はちょっと長くなったので、これまでとしておきます。

「内包表記でのアンパッキングとか意味分からん!」と思った方も、extendメソッドやupdateメソッドを使って書き下せることを理解すれば、「あー、そういうものね!」となるのではないでしょうか。ボクはそうでした。最初に見たときには「まー、分からんではないけれど……」となっていたのがちょっとスッキリしました。皆さんもちょっとスッキリしてくれたらいいな。

導入する・自動化する・分析する・作る ― AI活用を始めよう@ITのDeep Insiderで

Copyright© Digital Advantage Corp. All Rights Reserved.

編集部注:この記事はAIを使用して作成されており、ITmedia NEWSの記事を元に、内容を変更せずにリライトしたものです。
Share Copied