モダンPython基礎 #4 コレクションと内包表記
#3 制御フロー の最後に少しだけ見た内包表記を本格的に扱う番です。その前に Python の 四つの中核コレクション — list、tuple、dict、set の使い分けから整理します。
一表で見る使い分け #
| 変更可能 | 順序 | 重複 | キー-値 | 表記 | |
|---|---|---|---|---|---|
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]
#
リストの真価はスライスにあります。
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 = pdict — キー-値マッピング
#
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(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)5 行が 1 行になります。読むのも速くなります — 一目で意図が分かります。
条件 — フィルタ #
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 |
いつ内包表記、いつ書き下し? #
内包表記が常に良いとは限りません。ロジックが複雑になれば書き下した方が読みやすくなります。
# 一行で可能だが、読みにくい
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 は一個まで くらいが内包表記が映える場面。それ以上は書き下す方が普通は良いです。
まとめ #
今回整理したもの:
list(可変、順序、重複) — スライス[start:stop:step]が強力tuple(不変、形が固定) — 多重戻り値・dict キー・NamedTupledict(キー-値、順序保証) —.get()、.items()、|でマージset(重複なし、順序なし) — 集合演算| & - ^- リスト / 辞書 / セット内包表記 —
[式 for x in iter if cond] if-elseは式の位置、ifはフィルタの位置- ジェネレータ式
(x for x in iter)— メモリ効率、一度だけ巡回 - 複雑になりすぎたら内包表記を書き下す方が良い
次回 (#5 関数 — 引数パターン) では関数定義の 多様な引数パターン を扱います。positional-only、keyword-only、*args / **kwargs のような表記まで。