内包表記(comprehension)は、forループとappendで書く処理を、1行で簡潔に書くPython特有の書き方です。[x * 2 for x in range(5)]のように書くと、[0, 2, 4, 6, 8]というリストが一発で作れます。Pythonらしいコードを書くうえで欠かせません。
つまずきやすいのは、条件のifを書く位置です。「絞り込み(フィルタ)」はifを末尾に、「値の変換(if-else)」は前に書きます。この違いを知らないとエラーになったり、意図と違う結果になったりします。この記事では、実機のPythonで確認しながら、内包表記の使い方を整理します。
- リスト内包表記は
[式 for 変数 in 反復対象]。for+appendを1行にできます。 - 実機の計測では、
for+appendより内包表記のほうが高速でした。 - 絞り込みは末尾に
if:[x for x in lst if 条件]。 - 変換(if-else)は前に:
[x if 条件 else 別の値 for x in lst]。 - 辞書は
{k: v for ...}、集合は{x for ...}でも書けます。 - 大きなデータには、
( )で書くジェネレータ式がメモリを節約できます。
内包表記はリストや辞書を作る書き方なので、リスト(list)の使い方、辞書(dict)の使い方、元になるforループで繰り返し処理を行う方法もあわせて読むと理解が深まります。
リスト内包表記の基本
基本の形は[式 for 変数 in 反復対象]です。forで取り出した変数を、前の「式」で加工した結果が、新しいリストになります。
# [式 for 変数 in 反復対象] [x * 2 for x in range(5)] # [0, 2, 4, 6, 8] # 文字列のリストを加工する names = ["yamada", "sato"] [name.upper() for name in names] # ['YAMADA', 'SATO'] # 既存リストから新しいリストを作る nums = [1, 2, 3, 4] [n ** 2 for n in nums] # [1, 4, 9, 16]
forループとの比較(速度・可読性)
内包表記は、forループ+appendとまったく同じことを短く書けます。並べて比べると分かりやすいです。
# for ループ+append(従来の書き方)
result = []
for x in range(5):
result.append(x * 2)
# result は [0, 2, 4, 6, 8]
# 内包表記(同じ結果を1行で)
result = [x * 2 for x in range(5)]
実機で100万件の処理を計測したところ、for+appendより内包表記のほうが速い結果になりました(内包表記は内部で最適化されているためです)。短く書けて速い、というのが内包表記の魅力です。ただし、処理が複雑になりすぎると逆に読みにくくなるため、後述のようにシンプルな加工に向いていると覚えておきましょう。
条件で絞り込む(末尾のif)
「条件に合うものだけ」を残したいときは、forの後ろ(末尾)にifを書きます。これがフィルタ(絞り込み)です。
# 偶数だけ残す(末尾の if で絞り込み) [x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8] # 負の数を除外する nums = [-2, 3, -1, 5] [x for x in nums if x > 0] # [3, 5] # 条件を複数つなげる [x for x in range(20) if x % 2 == 0 if x > 5] # [6, 8, 10, ...]
実機でも、[x for x in nums if x > 0]は負の数を除いた[3, 5]になりました。末尾のifは「残すかどうか」を決めるフィルタです。条件を満たさない要素は、結果のリストに入りません。
【重要】if-elseは前に置く
ここが混乱しやすいポイントです。「条件によって値を変える」場合はif-elseを使いますが、これは前(式の部分)に書きます。末尾のif(フィルタ)とは位置も意味も違います。
nums = [-2, 3, -1, 5] # 末尾の if(フィルタ): 負の数を「除外」する → 数が減る [x for x in nums if x > 0] # [3, 5] # 前の if-else(変換): 負の数を「0に置き換える」 → 数は同じ [x if x > 0 else 0 for x in nums] # [0, 3, 0, 5]
実機で確認したところ、[x for x in nums if x > 0]は負の数を除外して[3, 5](要素が減る)、[x if x > 0 else 0 for x in nums]は負の数を0に変換して[0, 3, 0, 5](要素数は同じ)になりました。「いらない要素を捨てる」なら末尾にifだけ、「全要素を残しつつ値を変える」なら前にif-elseです。if-elseを末尾に書くとエラー(構文エラー)になり、末尾のifにelseを付けることはできません。位置で意味が変わると覚えてください。
辞書内包表記・集合内包表記
内包表記はリストだけでなく、辞書や集合でも使えます。波かっこ{ }を使い、辞書はキー: 値、集合は値だけを書きます。
# 辞書内包表記 {キー: 値 for ...}
{x: x ** 2 for x in range(4)} # {0: 0, 1: 1, 2: 4, 3: 9}
# 辞書のキーと値を入れ替える
d = {"a": 1, "b": 2}
{v: k for k, v in d.items()} # {1: 'a', 2: 'b'}
# 集合内包表記 {値 for ...}(重複は自動で除かれる)
{x % 3 for x in range(10)} # {0, 1, 2}
# 条件も付けられる
{x: x ** 2 for x in range(10) if x % 2 == 0}
実機でも、辞書内包で{0: 0, 1: 1, 2: 4, 3: 9}、キーと値の入れ替えで{1: 'a', 2: 'b'}、集合内包で重複を除いた{0, 1, 2}が得られました。辞書内包は「リストや別の辞書から、新しい辞書を一気に作る」のに便利です。
ネストした内包表記
内包表記の中にforを2つ書くと、ネスト(入れ子)したループになります。順番は外側のループを先に、内側を後に書きます。
# 2つの for を並べる(外側 → 内側の順) [(x, y) for x in range(2) for y in range(2)] # [(0, 0), (0, 1), (1, 0), (1, 1)] # 2次元リストを平らにする(flatten) matrix = [[1, 2], [3, 4], [5, 6]] [n for row in matrix for n in row] # [1, 2, 3, 4, 5, 6]
実機でも、2つのforを並べると、外側のループが先に回り、内側が後に回りました(通常の入れ子ループと同じ順番です)。2次元リストを1次元にする「flatten(平坦化)」は、この書き方の定番です。ただし、ネストが深くなると一気に読みにくくなるため、2段までを目安にするとよいでしょう。
ジェネレータ式(()でメモリ節約)
角かっこ[ ]の代わりに丸かっこ( )を使うと、ジェネレータ式になります。リスト内包がすべての結果を一度にメモリへ作るのに対し、ジェネレータ式は必要なときに1つずつ作るため、大きなデータでメモリを節約できます。
# リスト内包: 全要素をメモリに作る
squares_list = [x ** 2 for x in range(1000000)] # 大量のメモリを使う
# ジェネレータ式: 1つずつ作る(メモリ節約)
squares_gen = (x ** 2 for x in range(1000000)) # ほぼメモリを使わない
# sum などに直接渡すなら、かっこを省略できる
total = sum(x ** 2 for x in range(1000000))
# ジェネレータは for で1つずつ取り出して使う
for value in (x * 2 for x in range(5)):
print(value) # 0, 2, 4, 6, 8
実機でも、(x ** 2 for x in range(...))の型はgeneratorで、sum(x ** 2 for x in range(...))のように集計関数へ直接渡せました。結果をリストとして何度も使うなら[ ]、合計などで1回だけ使うなら( )のジェネレータ式が効率的です。巨大なファイルやデータを扱うときに役立ちます。
可読性の境界(使いすぎ注意)
内包表記は便利ですが、詰め込みすぎると読めなくなります。条件やネストが多いときは、無理に1行にせず、普通のforループに戻すほうが読みやすいこともあります。
# 読みにくい例(条件もネストも多い)
result = [f(x, y) for x in data for y in x.items if y > 0 if x.valid]
# こういうときは for ループに戻すほうが読みやすい
result = []
for x in data:
if not x.valid:
continue
for y in x.items:
if y > 0:
result.append(f(x, y))
目安として、「式が単純」かつ「ネストは2段まで」かつ「条件は1つまで」なら内包表記、それを超えるならforループ、と考えると読みやすさを保てます。短く書けることより、読みやすさを優先してください。
よくある失敗
if-elseを末尾に書いてエラーになる
変換のif-elseは前(式の部分)に書きます。末尾のifはフィルタ専用で、elseは付けられません。
フィルタと変換を取り違える
「いらない要素を捨てる」なら末尾if、「全要素を残して値を変える」なら前のif-elseです。結果の要素数が変わるかどうかで見分けられます。
ネストの順番を逆にする
2つのforは「外側 → 内側」の順で書きます。通常のネストしたループと同じ順番です。
大きなデータをリスト内包で一度に作る
巨大なデータをリスト内包で作るとメモリを大量に使います。合計など1回だけ使うなら、( )のジェネレータ式を使います。
複雑な処理を無理に1行にする
条件やネストが多いと読めなくなります。複雑なときは普通のforループに戻すほうが読みやすいです。
よくある質問
ifは末尾に、値を変えるif-elseは前(式の部分)に書きます。[x for x in lst if 条件]は条件に合うものだけ残し、[x if 条件 else 別 for x in lst]は全要素を残しつつ値を変えます。forループのほうが読みやすくなります。読みやすさを優先して選びます。{キー: 値 for 変数 in 反復対象}と書きます。たとえば{x: x**2 for x in range(4)}で{0: 0, 1: 1, 2: 4, 3: 9}になります。集合なら{値 for ...}です。[ ]のリスト内包は全要素を一度にメモリへ作り、( )のジェネレータ式は必要なときに1つずつ作ります。結果を何度も使うならリスト、合計など1回だけ使う・巨大なデータならジェネレータ式がメモリを節約できます。[n for row in matrix for n in row]のように、forを2つ並べます。外側で各行を取り出し、内側で各要素を取り出すことで、平らな(1次元の)リストになります。まとめ
- リスト内包表記は
[式 for 変数 in 反復対象]。for+appendを1行にでき、実機計測でも高速でした。 - 絞り込みは末尾に
if、変換は前にif-elseです。位置で意味が変わります。 - 辞書は
{k: v for ...}、集合は{x for ...}でも書けます。 - ネストは「外側 → 内側」の順。flatten(平坦化)の定番です。
- 大きなデータには、
( )のジェネレータ式でメモリを節約します。 - 複雑すぎるときは、無理に1行にせず
forループに戻します。
内包表記は、Pythonらしく短く速いコードを書くための強力な道具です。「フィルタは末尾if、変換は前if-else」という位置の違いさえ押さえれば、リストや辞書をすっきり作れるようになります。読みやすさを保つ範囲で活用してください。
