Pythonのリスト内包表記を完全に理解する|初心者がつまずきやすいポイントも解説

7 min 91 views
内包表記

Pythonを触り始めると、ほぼ必ず目にする構文のひとつが リスト内包表記(list comprehension)です。
一見すると難しそうに見えますが、実はコードを「短く」「読みやすく」書けるとても便利な機能です。

この記事では、リスト内包表記の基本から、条件分岐・zip/enumerateとの組み合わせ・ネスト構造・集合/辞書内包表記・ジェネレータ式まで、段階的に理解できるように丁寧に解説します。

「for 文で書けるけど、もっと Python らしく書きたい」
そんな方にぜひ読んでほしい内容です。

■ リスト内包表記とは?

リスト内包表記とは、

「イテラブル(for で回せる値)に対して処理を行い、新しいリストを一行で作る書き方」

のことです。

基本の形は次のとおり。

[式 for 変数 in イテラブル]

例えば、0〜4 の数字を2乗したリストを作りたい場合:

squares = [i**2 for i in range(5)]
# → [0, 1, 4, 9, 16]

同じ処理を普通の for 文で書くとこうなります。

squares = []
for i in range(5):
    squares.append(i**2)

比べると、リスト内包表記の方が圧倒的に簡潔ですよね。
Python では「短くて読みやすいコード」が推奨されるため、よく使われる書き方です。

■ 条件をつけて抽出する(if の後置)

リスト内包表記では、特定の条件を満たす要素だけ取り出すこともできます。
基本形は次の通り。

[式 for 変数 in イテラブル if 条件式]

例:0〜9 の数字から奇数だけを集める

odds = [i for i in range(10) if i % 2 == 1]
# → [1, 3, 5, 7, 9]

for 文で書くとこうなります。

odds = []
for i in range(10):
    if i % 2 == 1:
        odds.append(i)

後置 if を使うことで、「条件に合うものだけを残す」処理が一行で書けます。

■ if…else を使った分岐処理(条件で値を変える)

さきほどの後置 if は「フィルタリング」でしたが、条件によって別の値を入れたいという場合もあります。

そのときは三項演算子(if else の一行版)を使います。

書き方はこちら。

[真のときの値 if 条件式 else 偽のときの値 for 変数 in イテラブル]

例:偶数は ‘even’、奇数は ‘odd’ に変換する

labels = ['odd' if i % 2 else 'even' for i in range(10)]

こういった「マッピング変換」では特に威力を発揮します。

■ zip() や enumerate() との組み合わせ

for 文ではよく zip() や enumerate() が使われますが、リスト内包表記でもそのまま活用できます。

 ● zip の例

names1 = ['a', 'b', 'c']
names2 = ['x', 'y', 'z']

pairs = [(n1, n2) for n1, n2 in zip(names1, names2)]
# → [('a', 'x'), ('b', 'y'), ('c', 'z')]

 ● enumerate の例

lst = ['apple', 'banana', 'cherry']

indexed = [(i, s) for i, s in enumerate(lst)]
# → [(0, 'apple'), (1, 'banana'), (2, 'cherry')]

複数の値を扱う処理が非常にスッキリ書けます。

■ ネストした内包表記(多重ループ)

for 文にネスト(入れ子)があるなら、リスト内包表記でも同じようにネストできます。

例:二次元リストを1次元へフラット化する

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

flat = [x for row in matrix for x in row]
# → [1,2,3,4,5,6,7,8,9]

これは最初につまずきやすい部分ですが、「for の順番はそのまま右に並ぶ」と覚えると理解しやすいです。

また複数のループで条件をつけることも可能です。

cells = [(r, c) for r in range(3) if r % 2 == 0
                    for c in range(2) if c % 2 == 0]
# → [(0,0), (2,0)]

■ 集合内包表記(set comprehensions)

リスト内包表記とほぼ同じ構文で、角括弧を波括弧 {} に変えると set(集合) を作れます。

s = {i**2 for i in range(5)}
# → {0,1,4,9,16}

集合は重複を持てないため、
自動的に重複要素がなくなります。

■ 辞書内包表記(dict comprehensions)

辞書(dict)も内包表記で簡単に作れます。

{キー: 値 for 変数 in イテラブル}

例:文字列と文字数の辞書

names = ['Alice', 'Bob', 'Charlie']
d = {s: len(s) for s in names}

zip と併用すれば、キーと値のリストから辞書が作れます。

keys = ['k1','k2','k3']
vals = [10,20,30]

d = {k:v for k, v in zip(keys, vals)}

■ ジェネレータ式(generator expressions)

リスト内包表記の []() に変えると、リストではなく ジェネレータ を返します。

g = (i**2 for i in range(5))

ジェネレータは「必要なときに1つずつ値を生成」するため、大量データ処理ではメモリ効率が抜群です。

例:sum の引数として使う場合、() は省略可能

sum(i**2 for i in range(5))
  • 全要素を使う処理 → 内包表記(リスト)が速いことが多い
  • 部分的にしか使わない処理 → ジェネレータが有利

という特徴があります。

■ まとめ:リスト内包表記は Python の表現力を広げる強力な武器

リスト内包表記は Python らしさの象徴でもあり、
「短く」「読みやすく」「効率的」なコードを書くための非常に重要なテクニックです。

この記事で紹介した内容を整理すると:

  • 基本形を覚えれば何にでも応用できる
  • 条件分岐(if)、三項演算子(if…else)も使える
  • zip や enumerate と相性が良い
  • ネスト構造で多重ループも表現できる
  • 集合や辞書も内包表記で作れる
  • 丸括弧ならジェネレータになる

最初はとっつきにくく感じても、使い慣れるほど「これが一番読みやすい」と実感できます。

追補:リスト内包表記の“実務で効く”レシピ集

 1) 取り出した値を使わない場合(プレースホルダとしての _

乱数で 1〜5 を 5 個つくる例。ループ変数を使わないときは _ を慣習的に使う と読みやすくなります。

from random import randint

random_nums = [randint(1, 5) for _ in range(5)]
print(random_nums)  # 例: [1, 5, 2, 1, 5]

分解ポイント

  • for _ in range(5): 5回まわすが、取り出す値は使わない意図を明示
  • randint(1, 5): 各回で 1〜5 の乱数を生成
    ※ テストで再現性が必要なら random.seed(0) などを先に呼ぶ

 2) 文字とコードポイントを往復する(chr / ord)

アルファベット a〜z を作る → それを ASCII コードに変換 する流れ。

# 'a'〜'z' のリスト
chars = [chr(n + ord('a')) for n in range(26)]
print(chars)
# ['a', 'b', 'c', ..., 'x', 'y', 'z']

# 文字列へ連結
s = ''.join(chars)
print(s)  # abcdefghijklmnopqrstuvwxyz

# 各文字の ASCII 値に変換
ords = [ord(c) for c in s]      # もちろん [ord(c) for c in chars] でもOK
print(ords)
# [97, 98, 99, 100, 101, ..., 120, 121, 122]

分解ポイント

  • ord(‘a’) は 97、chr(97) は ‘a’
  • n + ord(‘a’) で 97〜122 を作り、chr(…) で文字へ
  • ”.join(chars) は 文字列 を得たいときの定番
    ※ この例は ASCII の小文字に限定。日本語など多言語は Unicode を意識(ord/chrはUnicodeに対応)

 3) enumerate で “位置+中身” をまとめて扱う

インデックスと値 を同時にタプルで持つ。UIリストの並び番号付与などに便利。

words = ['foo', 'bar', 'baz']
indexed_words = [(idx, word) for idx, word in enumerate(words)]
print(indexed_words)
# [(0, 'foo'), (1, 'bar'), (2, 'baz')]

分解ポイント

  • enumerate(words) は (0,’foo’), (1,’bar’), … を順に返す
  • [(idx, word) for …] の形で “構造の保存” が分かりやすい
    小ワザ:1始まりにしたいなら enumerate(words, start=1)

 4) zip で “要素ごとの演算” を一行に

2 つの等長リストを 要素ごと に加算していく。データ列の合成に最適。

nums1 = [1, 2, 3, 4, 5]
nums2 = [5, 4, 3, 2, 1]
sums = [a + b for a, b in zip(nums1, nums2)]
print(sums)  # [6, 6, 6, 6, 6]

分解ポイント

  • zip(nums1, nums2) は (1,5), (2,4), … のペアを作る
  • a + b をそのまま内包表記で評価
    注意:長さが違うと短い方に揃って切り捨てられる(itertools.zip_longestで補完も可)

 5) 多重 for で直積(ペアの全組み合わせ)

2つの範囲の全組み合わせ を作る。座標やテストケース生成に役立つ。

nums = [[i, j] for i in range(3) for j in range(2)]
print(nums)
# [[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1]]

分解ポイント(順番に注意)

  • for i in range(3) が 外側ループ
  • 続く for j in range(2) が 内側ループ
  • 内包表記は 左から右 にループがネストしていく
    等価な for 文の順番と完全一致させると迷いにくい

 6) ネスト内包で 2 次元表を作る(九九 1〜5 の段)

行(段)×列(掛ける数)を 二重の内包表記 で生成します。

multbl = [[x * y for y in range(1, 10)]   # 1〜9 列
          for x in range(1, 6)]           # 1〜5 行(段)

for row in multbl:
    print(row)
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# [2, 4, 6, 8, 10, 12, 14, 16, 18]
# [3, 6, 9, 12, 15, 18, 21, 24, 27]
# [4, 8, 12, 16, 20, 24, 28, 32, 36]
# [5, 10, 15, 20, 25, 30, 35, 40, 45]

分解ポイント

  • 外側の for x in range(1, 6) が 行(段) の生成
  • 内側の [x * y for y in range(1, 10)] が 列(1〜9) の各要素
  • 結果は「リストのリスト」=2 次元データ

応用:見やすく整形して表示

for row in multbl:
    print(' '.join(f'{n:>2}' for n in row))
  • f'{n:>2}’ は 幅2で右寄せ(桁がそろって見やすい)

 使い分けの勘所(今回の追補で出てきた観点)

  • _ は「値を使いません」の合図:読み手に親切で、リンター警告も避けやすい
  • 文字 ↔ コードポイント:chr/ord で往復できる。ASCII 限定なら ord(‘a’) バイアスがシンプル
  • enumerate と zip:タプルで“構造を持ったまま”加工できる。enumerate(…, start=1) や zip_longest の使い分けも覚えると強い
  • 多重内包:順序は 左から右が外→内。読みづらいと感じたら 一度 for 文に戻して から再内包化すると安全
  • 2 次元生成:外側=行、内側=列、と言語化しておくと迷わない

関連記事