Python 3.15に追加されるlazy importと内包表記でのアンパッキングについて調べてみたHPかわさきの研究ノート

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

» 2026年05月19日 05時00分 公開
[かわさきしんじDeep Insider編集部]

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

「HPかわさきの研究ノート」のインデックス

連載目次


かわさき

 どうもHPかわさきです。

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


lazy import

 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の使用例 lazy importの使用例

 といっても、これでは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ファイルとmodb.pyファイル

 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」を実行したところです。

モジュールをインポートした時点でprint関数が呼び出されてコンソールに出力が行われる モジュールをインポートした時点でprint関数が呼び出されてコンソールに出力が行われる

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

lazy importしたのでコンソールへの出力はモジュールを使い始めた後に変わっている 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 importとする
  • none:lazy importを無効化する(lazyを指定してもインポートは遅延されない)
  • normal:lazyを付加しているimport文はlazy importして、lazyがないimport文ではすぐにモジュールがロードされる

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

 これについてもちょっと試しておきましょう。

「-X lazy_imports=all」を指定したので「import moda」「from modb import func_b」でもモジュールのロードが遅延されている 「-X lazy_imports=all」を指定したので「import moda」「from modb import func_b」でもモジュールのロードが遅延されている

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

sys.set_lazy_imports関数とsys.get_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文を実行すると、フィルター関数が呼び出されないようになっていました。この辺の挙動には少し注意が必要かもしれません。

フィルター関数を設定しても、グローバル指示でデフォルト遅延になっていないと、シンプルな「import ...」ではフィルター関数が呼び出されない フィルター関数を設定しても、グローバル指示でデフォルト遅延になっていないと、シンプルな「import ...」ではフィルター関数が呼び出されない

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

__lazy_modules__にロードを遅延したモジュールを列挙する __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.14までのコード

 このコードが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)

リスト内包表記と対応するfor文

 では、内包表記でのアンパッキングを使ったコードはどのようにして書き下せるのでしょうか。といっても、リスト内包表記では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

リスト内包表記でのアンパッキングはfor文の内部でextendメソッドを使うのに相当する

 じゃあ、集合と辞書はどうなるでしょう。その場合は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

集合と辞書ではfor文の内部でupdateメソッドで更新を行うことに相当する

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

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

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


かわさき

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


「HPかわさきの研究ノート」のインデックス

HPかわさきの研究ノート

Copyright© Digital Advantage Corp. All Rights Reserved.

アイティメディアからのお知らせ

スポンサーからのお知らせPR

注目のテーマ

その「AIコーディング」は本当に必要か?
Microsoft & Windows最前線2026
4AI by @IT - AIを作り、動かし、守り、生かす
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。