【Python】内包表記の使い方|リスト内包・辞書内包・条件付き・ネスト

【Python】内包表記の使い方|リスト内包・辞書内包・条件付き・ネスト Python

内包表記(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 反復対象]forappendを1行にできます。
  • 実機の計測では、forappendより内包表記のほうが高速でした。
  • 絞り込みは末尾に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で取り出した変数を、前の「式」で加工した結果が、新しいリストになります。

basic.py
# [式 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とまったく同じことを短く書けます。並べて比べると分かりやすいです。

vs-for.py
# 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万件の処理を計測したところ、forappendより内包表記のほうが速い結果になりました(内包表記は内部で最適化されているためです)。短く書けて速い、というのが内包表記の魅力です。ただし、処理が複雑になりすぎると逆に読みにくくなるため、後述のようにシンプルな加工に向いていると覚えておきましょう。

条件で絞り込む(末尾のif)

「条件に合うものだけ」を残したいときは、for後ろ(末尾)ifを書きます。これがフィルタ(絞り込み)です。

filter.py
# 偶数だけ残す(末尾の 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(フィルタ)とは位置も意味も違います。

if-else.py
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]
絞り込みは末尾if、変換はif-else前置

実機で確認したところ、[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を末尾に書くとエラー(構文エラー)になり、末尾のifelseを付けることはできません。位置で意味が変わると覚えてください。

辞書内包表記・集合内包表記

内包表記はリストだけでなく、辞書や集合でも使えます。波かっこ{ }を使い、辞書はキー: 値、集合は値だけを書きます。

dict-set.py
# 辞書内包表記 {キー: 値 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つ書くと、ネスト(入れ子)したループになります。順番は外側のループを先に、内側を後に書きます。

nested.py
# 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つずつ作るため、大きなデータでメモリを節約できます。

generator.py
# リスト内包: 全要素をメモリに作る
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ループに戻すほうが読みやすいこともあります。

readability.py
# 読みにくい例(条件もネストも多い)
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ループに戻すほうが読みやすいです。

よくある質問

Qifを書く位置はどこですか?
A絞り込み(フィルタ)のif末尾に、値を変えるif-else前(式の部分)に書きます。[x for x in lst if 条件]は条件に合うものだけ残し、[x if 条件 else 別 for x in lst]は全要素を残しつつ値を変えます。
Q内包表記とforループはどちらを使うべきですか?
Aシンプルな加工や絞り込みなら内包表記が短く速いです。実機計測でも内包表記のほうが高速でした。一方、条件やネストが多く読みにくくなる場合は、普通のforループのほうが読みやすくなります。読みやすさを優先して選びます。
Q辞書も内包表記で作れますか?
A作れます。{キー: 値 for 変数 in 反復対象}と書きます。たとえば{x: x**2 for x in range(4)}{0: 0, 1: 1, 2: 4, 3: 9}になります。集合なら{値 for ...}です。
Qジェネレータ式とリスト内包の違いは?
A[ ]のリスト内包は全要素を一度にメモリへ作り、( )のジェネレータ式は必要なときに1つずつ作ります。結果を何度も使うならリスト、合計など1回だけ使う・巨大なデータならジェネレータ式がメモリを節約できます。
Q2次元リストを1次元にするには?
A[n for row in matrix for n in row]のように、forを2つ並べます。外側で各行を取り出し、内側で各要素を取り出すことで、平らな(1次元の)リストになります。

まとめ

  • リスト内包表記は[式 for 変数 in 反復対象]forappendを1行にでき、実機計測でも高速でした。
  • 絞り込みは末尾にif、変換は前にif-elseです。位置で意味が変わります。
  • 辞書は{k: v for ...}、集合は{x for ...}でも書けます。
  • ネストは「外側 → 内側」の順。flatten(平坦化)の定番です。
  • 大きなデータには、( )のジェネレータ式でメモリを節約します。
  • 複雑すぎるときは、無理に1行にせずforループに戻します。

内包表記は、Pythonらしく短く速いコードを書くための強力な道具です。「フィルタは末尾if、変換は前if-else」という位置の違いさえ押さえれば、リストや辞書をすっきり作れるようになります。読みやすさを保つ範囲で活用してください。