目次
4 章

コレクションと内包表記

list/tuple/dict/set の 4 コレクションの使いどころと、一行で新しいコレクションを作る内包表記・ジェネレータ式までまとめます。

第3章 制御フロー の終わりにちらっと見た内包表記を、ここで改めて扱います。その前に Python の 四つの基本コレクションlisttupledictset の使いどころから整理します。

本章の二つの軸は (1) 四つのコレクションの違いを正確に知ること、(2) 内包表記を一行で書く感覚に慣れることです。どちらも本書全体で繰り返し使う道具です。第11章 イテラブル、ジェネレータ、yield from では、本章の内包表記とジェネレータ式が同じイテレータプロトコルの上にどう乗っているかを詳しく見ます。

一表で見る比較 #

変更可能順序重複キー-値表記
listOO許可X[1, 2, 3]
tupleXO許可X(1, 2, 3)
setOX不可X{1, 2, 3}
dictOキー uniqueO{"a": 1}

¹ Python 3.7+ から dict は挿入順序を保証します。

list — もっともよく使うコレクション #

list 基本
nums: list[int] = [1, 2, 3]

nums.append(4)         # [1, 2, 3, 4]
nums.insert(0, 0)      # [0, 1, 2, 3, 4]
nums.remove(2)         # [0, 1, 3, 4]
last = nums.pop()      # 4 を返し、リストは [0, 1, 3]

print(len(nums))       # 3
print(nums[0])         # 0
print(3 in nums)       # True

スライス — [start:stop:step] #

list はスライスと組み合わせると特に便利です。

スライス
items = [10, 20, 30, 40, 50]

items[1:3]    # [20, 30]    1 以上 3 未満
items[:2]     # [10, 20]    先頭から 2 未満
items[3:]     # [40, 50]    3 以上から末尾まで
items[-2:]    # [40, 50]    末尾から 2 個
items[::2]    # [10, 30, 50]  step 2
items[::-1]   # [50, 40, 30, 20, 10]   反転

step = -1 でリストを反転させるパターンは頻出です。JavaScript の arr.toReversed() よりも短く速いです。

スライスは 新しいリストを作って返します。 元のリストは変わりません。

+* — 結合と反復 #

結合と反復
[1, 2] + [3, 4]   # [1, 2, 3, 4]
[0] * 5           # [0, 0, 0, 0, 0]

[0] * 5 は 0 で埋めたリストを作るもっとも短い方法です。ただし 参照型を掛けると全部が同じオブジェクト になります。

落とし穴 — 可変オブジェクトの掛け算
matrix = [[]] * 3
matrix[0].append(1)
print(matrix)
# [[1], [1], [1]]   ← すべて同じリストを指している!

この場合は内包表記を使う必要があります (下で扱います) 。

tuple — 形が決まった束 #

list とほぼ同じですが 変更不可 です。代わりに使いどころが異なります。

tuple 基本
point: tuple[float, float] = (1.0, 2.0)
person: tuple[str, int] = ("カーティス", 30)

# tuple は通常 unpack して使う
name, age = person
print(name, age)   # カーティス 30

# 一要素 tuple はカンマ必須 — 括弧だけでは不足
single = (42,)     # tuple
not_tuple = (42)   # ただの整数

tuple が向く場面 #

  • 複数の値を一緒に返す とき: return name, age (実は tuple)
  • 辞書のキー: list はキーになれないが tuple はなれる (不変だから)
  • 形が決まった座標 / 日付のようなデータ — list より意図がはっきり

より明示的な tuple — NamedTuple #

位置で決まる tuple は時間が経つと迷います (person[0] は名前だったかな?) 。名前を付けた tuple を使えばすっきりします。

NamedTuple
from typing import NamedTuple

class Person(NamedTuple):
    name: str
    age: int

p = Person("カーティス", 30)
print(p.name, p.age)   # カーティス 30

# tuple のように unpack も可
name, age = p

NamedTuple よりも一般的に使われる @dataclass は第8章 dataclass と __slots__ で詳しく扱います。変更可能なデータ、メソッドの定義、検証などが必要なら dataclass の方が自然です。

dict — キー-値マッピング #

dict 基本
user: dict[str, int] = {"id": 1, "age": 30}

print(user["id"])              # 1
print(user.get("nope"))        # None  (ないキー → 安全)
print(user.get("nope", -1))    # -1    (デフォルト値)

user["name"] = "curtis"        # 追加/更新
del user["age"]                # 削除
print("name" in user)          # True

user["ないキー"]KeyError 例外 を投げます。安全に取り出すには .get() を使ってください。

走査 #

dict 走査
for key in user:                # キーだけ
    print(key)

for key, value in user.items(): # 両方
    print(key, value)

for value in user.values():     # 値だけ
    print(value)

.items() はほぼ毎日使います。

結合 — | 演算子 (3.9+) #

dict 結合
defaults = {"a": 1, "b": 2}
overrides = {"b": 20, "c": 30}

merged = defaults | overrides
print(merged)    # {"a": 1, "b": 20, "c": 30}

| は右が勝ちます — 同じキーなら後ろの値が残ります。JavaScript の {...defaults, ...overrides} と同じ意味です。

set — 重複なしの束 #

set 基本
unique: set[int] = {1, 2, 3, 2, 1}
print(unique)   # {1, 2, 3}

unique.add(4)      # {1, 2, 3, 4}
unique.discard(2)  # {1, 3, 4}
print(3 in unique) # True

# 空 set は set() — {} は空 dict
empty = set()

集合演算 #

集合演算
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b   # 和集合  {1, 2, 3, 4, 5, 6}
a & b   # 積集合  {3, 4}
a - b   # 差集合  {1, 2}
a ^ b   # 対称差  {1, 2, 5, 6}

list の重複除去のもっとも短い方法: list(set(items))。ただし 順序が崩れる可能性があります。 順序を保ったまま重複を除くには次の通りです。

順序を保った重複除去
items = [1, 2, 1, 3, 2, 4]
unique = list(dict.fromkeys(items))
print(unique)   # [1, 2, 3, 4]

dict.fromkeys() が dict の順序保持 + キー unique の性質を同時に活用します。

内包表記 — 一行でコレクションを作る #

for ループと条件を一行に統合する文法です。Python でもっともよく使うパターンの一つ です。

リスト内包表記 #

基本形
# [式 for 変数 in イテラブル]

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

for ループでばらすと次の通りです。

同じコード、ばらして
squares = []
for x in range(5):
    squares.append(x ** 2)

五行が一行に縮みます。読むのも速くなります — 一度に意図が見えます。

条件 — フィルタ #

if 節
evens = [x for x in range(10) if x % 2 == 0]
# [0, 2, 4, 6, 8]

if はフィルタです。条件に合う要素だけ入ります。

変換 + フィルタ同時 #

両方同時
# 偶数だけ選んで二乗
result = [x ** 2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]

if-else 式 (位置に注意) #

if-else式の位置 に入ります。フィルタの if とは位置が違います。

条件式
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
# ['even', 'odd', 'even', 'odd', 'even']

順序は [式 if cond else 別の式 for x in iter] です。迷いやすい部分なのでゆっくり読んでください。

ネスト — 2D 作り #

ネスト内包表記 — 2D 行列
matrix = [[0 for _ in range(3)] for _ in range(3)]
# [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

前に見た [[0]] * 3 の落とし穴を内包表記が解決します。毎回新しいリストが作られるので互いに独立です。

辞書内包表記 #

dict comprehension
square_map = {x: x ** 2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# キー-値 swap
user = {"id": 1, "name": "curtis"}
swapped = {v: k for k, v in user.items()}
# {1: "id", "curtis": "name"}

セット内包表記 #

set comprehension
unique_lengths = {len(w) for w in ["a", "bb", "cc", "ddd"]}
# {1, 2, 3}

落とし穴 — ネスト for の順序 #

ネスト for
pairs = [(x, y) for x in [1, 2] for y in ['a', 'b']]
# [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]

複数の for があるとき 左側が外側ループ です。ばらすと次の通りです。

同じ動作のばらし書き
pairs = []
for x in [1, 2]:
    for y in ['a', 'b']:
        pairs.append((x, y))

ジェネレータ式 — メモリを節約する #

角括弧の代わりに 丸括弧 を使うと内包表記ではなく ジェネレータ になります。

ジェネレータ式
gen = (x ** 2 for x in range(1_000_000))
print(gen)   # <generator object ...>

リスト内包表記は すぐに全要素を作ります。 100 万個なら 100 万個分のメモリを取ります。ジェネレータは 要求があったときだけ次の値を作る ため、メモリがほとんどかかりません。

合計 — ジェネレータで十分
total = sum(x ** 2 for x in range(1_000_000))
# 関数に直接入れるときは () を省略できる

summaxanyall のように 一度走査すれば済む関数に渡すとき が、ジェネレータの適切な用途です。

リスト内包表記ジェネレータ式
表記[ ... ]( ... )
メモリ全要素を即時生成必要なときだけ
再利用何度も走査可一度だけ 走査可
インデックスアクセスO result[3]X

ジェネレータのより深い使い方 — yieldyield from、async generator — は第11章 イテラブル、ジェネレータ、yield from で扱います。

いつ内包表記、いつばらし書き? #

内包表記が常によいわけではありません。ロジックが複雑になると、ばらして書く方が読みやすいです。

これはばらそう
# 一行で可能だが読みにくい
result = [transform(x) for x in items if validate(x) and is_active(x) and not is_deleted(x)]

# ばらして書くとデバッグもしやすく読みやすい
result = []
for x in items:
    if not validate(x):
        continue
    if not is_active(x) or is_deleted(x):
        continue
    result.append(transform(x))

判断基準: 一行に一式、if は一つまで くらいが内包表記の輝く範囲です。それ以上は普通ばらした方がよいです。

練習問題 #

  1. users: list[dict[str, int | str]][{"name": "a", "age": 30}, {"name": "b", "age": 17}, {"name": "c", "age": 22}] の形で与えられます。内包表記一行で 成人(19 以上)の名前だけ選んで list で 返す式を書いてください。
  2. words = ["apple", "banana", "cherry", "date"] から dict 内包表記で {"apple": 5, "banana": 6, "cherry": 6, "date": 4} のような単語 → 長さのマッピングを作ってください。
  3. range(1, 100_000_001) の偶数の二乗の合計を求める必要があります。(1) リスト内包表記 sum([x**2 for x in range(...) if x % 2 == 0]) で一度、(2) ジェネレータ式 sum(x**2 for x in range(...) if x % 2 == 0) で一度求めてみてください。メモリ / 時間の差を直接体感します (メモリ監視は第21章で再度扱います) 。

一行まとめ: 四つのコレクションの選択は変更可能性 / 順序 / 重複 / キー-値 の 4 軸で分かれる。list のスライス、dict.get.items()set の集合演算が日常の 90% を占める。内包表記一行は通常より短く速いが、条件 / 変換が複雑になるとばらした方がよい。メモリ負担のある一度きりの走査はジェネレータ式 (...) で。

次の章 #

次の 第5章 関数 — 引数パターン では、関数定義の 多様な引数パターン を扱います。位置 / キーワード / デフォルト値 / *args / **kwargs / 位置専用 / キーワード専用までです。本章の内包表記と組み合わさると、関数型スタイルのデータ変換コードを短く書けるようになります。

X