コレクションと内包表記
list/tuple/dict/set の 4 コレクションの使いどころと、一行で新しいコレクションを作る内包表記・ジェネレータ式までまとめます。
第3章 制御フロー の終わりにちらっと見た内包表記を、ここで改めて扱います。その前に Python の 四つの基本コレクション — list、tuple、dict、set の使いどころから整理します。
本章の二つの軸は (1) 四つのコレクションの違いを正確に知ること、(2) 内包表記を一行で書く感覚に慣れることです。どちらも本書全体で繰り返し使う道具です。第11章 イテラブル、ジェネレータ、yield from では、本章の内包表記とジェネレータ式が同じイテレータプロトコルの上にどう乗っているかを詳しく見ます。
一表で見る比較 #
| 変更可能 | 順序 | 重複 | キー-値 | 表記 | |
|---|---|---|---|---|---|
list | O | O | 許可 | X | [1, 2, 3] |
tuple | X | O | 許可 | X | (1, 2, 3) |
set | O | X | 不可 | X | {1, 2, 3} |
dict | O | O¹ | キー unique | O | {"a": 1} |
¹ Python 3.7+ から dict は挿入順序を保証します。
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 とほぼ同じですが 変更不可 です。代わりに使いどころが異なります。
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 を使えばすっきりします。
from typing import NamedTuple
class Person(NamedTuple):
name: str
age: int
p = Person("カーティス", 30)
print(p.name, p.age) # カーティス 30
# tuple のように unpack も可
name, age = pNamedTuple よりも一般的に使われる @dataclass は第8章 dataclass と __slots__ で詳しく扱います。変更可能なデータ、メソッドの定義、検証などが必要なら dataclass の方が自然です。
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) # Trueuser["ないキー"] は KeyError 例外 を投げます。安全に取り出すには .get() を使ってください。
走査 #
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+)
#
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 — 重複なしの束
#
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)五行が一行に縮みます。読むのも速くなります — 一度に意図が見えます。
条件 — フィルタ #
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 作り #
matrix = [[0 for _ in range(3)] for _ in range(3)]
# [[0, 0, 0], [0, 0, 0], [0, 0, 0]]前に見た [[0]] * 3 の落とし穴を内包表記が解決します。毎回新しいリストが作られるので互いに独立です。
辞書内包表記 #
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"}セット内包表記 #
unique_lengths = {len(w) for w in ["a", "bb", "cc", "ddd"]}
# {1, 2, 3}落とし穴 — ネスト 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))
# 関数に直接入れるときは () を省略できるsum、max、any、all のように 一度走査すれば済む関数に渡すとき が、ジェネレータの適切な用途です。
| リスト内包表記 | ジェネレータ式 | |
|---|---|---|
| 表記 | [ ... ] | ( ... ) |
| メモリ | 全要素を即時生成 | 必要なときだけ |
| 再利用 | 何度も走査可 | 一度だけ 走査可 |
| インデックスアクセス | O result[3] | X |
ジェネレータのより深い使い方 — yield、yield 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 は一つまで くらいが内包表記の輝く範囲です。それ以上は普通ばらした方がよいです。
練習問題 #
users: list[dict[str, int | str]]が[{"name": "a", "age": 30}, {"name": "b", "age": 17}, {"name": "c", "age": 22}]の形で与えられます。内包表記一行で 成人(19 以上)の名前だけ選んで list で 返す式を書いてください。words = ["apple", "banana", "cherry", "date"]から dict 内包表記で{"apple": 5, "banana": 6, "cherry": 6, "date": 4}のような単語 → 長さのマッピングを作ってください。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 / 位置専用 / キーワード専用までです。本章の内包表記と組み合わさると、関数型スタイルのデータ変換コードを短く書けるようになります。