【ITニュース解説】Comprehension in Python (4)
2025年09月28日に「Dev.to」が公開したITニュース「Comprehension in Python (4)」について初心者にもわかりやすく解説しています。
ITニュース概要
Pythonのリスト、タプル、セット、辞書を簡潔に生成する「内包表記」と、メモリ効率の良い「ジェネレータ」を解説する。特にジェネレータ内包表記は、1次元から3次元の多次元データ処理でも簡潔に記述できることを豊富なコード例で示している。
ITニュース解説
Pythonプログラミングにおいて、効率的なデータ処理は非常に重要な要素だ。特に大量のデータを扱う際や、計算コストの高い処理を実行する際には、メモリの使用量を抑え、処理速度を向上させる工夫が求められる。そのような場面で活躍するのが、今回解説する「ジェネレータ内包表記」だ。これは、Pythonに備わる強力な機能の一つである内包表記の仲間で、ジェネレータという特殊なオブジェクトを生成するために使われる。
まず、ジェネレータとは何かを理解することから始めよう。Pythonでは、リストやタプルといったデータ構造は、すべての要素をメモリ上に保持する。例えば、1から10億までの数字を含むリストを作成しようとすると、その数字すべてをメモリに格納するため、膨大なメモリを消費してしまう。ジェネレータはこれとは異なり、要素を一度にすべて生成してメモリに保持するのではなく、「必要になったときに、その都度一つずつ値を生成する」という特徴を持つ。これは「遅延評価」や「オンデマンド生成」とも呼ばれる考え方で、メモリ使用量を大幅に削減できるという大きな利点がある。ジェネレータは、yieldキーワードを持つ関数、または今回説明するジェネレータ内包表記によって作成される。
ジェネレータ内包表記は、リスト内包表記とよく似た構文を持つが、角括弧 [] の代わりに丸括弧 () を使用する点が異なる。例えば、v = (x**2 for x in [0, 1, 2, 3, 4, 5, 6, 7]) というコードは、与えられたリストの各要素を二乗した値を生成するジェネレータオブジェクト v を作成する。この時点では、二乗された値はまだ一つも計算されておらず、メモリにも格納されていない。v は、次に値を要求されたときに、最初の要素 0 を取り出し、0**2 を計算して 0 を生成する。次に要求されたら 1**2 を計算して 1 を生成するといった具合に、必要なときに必要な分だけ値を生成していく。そのため、for x in v: のようなループでジェネレータオブジェクトから値を取り出すと、0, 1, 4, 9, 16, 25, 36, 49 と順に結果が表示される。
ジェネレータ内包表記の括弧に関する注意点もある。基本的にジェネレータ内包表記は丸括弧で囲む必要がある。例えば、print(x**2 for x in range(8)) と記述すると、() が省略されているため、これはジェネレータオブジェクトそのもの(<generator object <genexpr> at ...>)が表示されるだけで、具体的な値は生成されない。値を生成して利用するには、やはり for ループなどでイテレーションする必要がある。さらに、v = x**2 for x in range(8) のように変数に直接代入しようとすると、SyntaxError となり、これはPythonの文法エラーとして扱われる。ただし、関数呼び出しの引数としてジェネレータ内包表記を渡す場合など、特定の状況では外側の括弧を省略できることもあるが、基本的には常に丸括弧で囲むと覚えておくと良い。
このジェネレータ内包表記は、従来のジェネレータ関数を簡潔に記述するためのものだ。例えば、上記と同じ処理をジェネレータ関数で書くと、以下のようになる。
1def func(s): 2 for x in s: 3 yield x**2 4 5sample = [0, 1, 2, 3, 4, 5, 6, 7] 6v = func(sample) 7for x in v: 8 print(x)
このように、yieldキーワードを使って関数を定義する必要がある。ジェネレータ内包表記を使えば、このfunc関数の定義に相当する部分を、v = (x**2 for x in sample) のように一行で記述できるため、コードがより短く、読みやすくなる。結果はどちらも同じだが、簡潔さという点でジェネレータ内包表記が優れている。
ジェネレータ内包表記は、多次元のデータ構造に対しても適用できる。入れ子になったリスト(リストの中にさらにリストがある構造)を処理する場合も、リスト内包表記と同様に、ジェネレータ内包表記を入れ子にできる。
例えば、sample = [[0, 1, 2, 3], [4, 5, 6, 7]] という2次元のリストがある場合、v = ((y**2 for y in x) for x in sample) と書くことで、2次元ジェネレータオブジェクトを作成できる。この式では、まず外側の ( ... for x in sample) が sample リストの各サブリスト x を取り出す。そして、内側の (y**2 for y in x) が、取り出された各サブリスト x に対して個別のジェネレータオブジェクトを生成する。結果として v は、複数のジェネレータオブジェクトを生成するジェネレータオブジェクトになる。これをループで処理するには、for x in v: の後にさらに for y in x: というように二重のループが必要となる。これにより、すべての要素が二乗されて出力される。
さらに複雑なsample = [[[0, 1], [2, 3]], [[4, 5], [6, 7]]]のような3次元のリストに対しても、v = (((z**2 for z in y) for y in x) for x in sample) のように三重にジェネレータ内包表記を入れ子にすることで、同様の処理が可能だ。この場合も、最も内側のジェネレータから順に値を生成していき、外側のジェネレータがそれを包み込む形で動作する。対応するジェネレータ関数を記述しようとすると、複数のyieldを含む関数を入れ子に定義することになり、コードがかなり複雑になることがわかるだろう。ジェネレータ内包表記は、このような複雑な処理を非常に簡潔な構文で表現できるため、コードの可読性を高め、開発効率を向上させる。
まとめると、ジェネレータ内包表記は、メモリ効率とコードの簡潔さを両立させるPythonの強力な機能だ。特に、大量のデータを扱うシステムや、無限ストリームのような継続的にデータを生成するシステムを構築するシステムエンジニアにとって、この遅延評価の特性は非常に有用となる。必要な時に必要なデータだけを生成することで、システムリソースを有効活用し、より高性能で安定したアプリケーションの開発に貢献するだろう。