Python の set vs list(いつ集合を使うべきか)
set と list は Python でよく使われるコレクション型だが、特性が大きく異なる。適切に使い分けることで、パフォーマンスとコードの明確さが向上する。
基本的な特性の違い
両者の根本的な違いを整理する。
順序あり、重複あり、インデックスアクセス可能、ミュータブル
順序なし、重複なし、インデックスアクセス不可、ミュータブル
この違いがすべての判断基準になる。
検索パフォーマンス
要素の存在確認は、set が圧倒的に高速だ。
import time
n = 100000
items_list = list(range(n))
items_set = set(range(n))
# list での検索
start = time.perf_counter()
for i in range(1000):
99999 in items_list
print(f"list: {time.perf_counter() - start:.4f}s")
# set での検索
start = time.perf_counter()
for i in range(1000):
99999 in items_set
print(f"set: {time.perf_counter() - start:.4f}s")
list は O(n)、set は O(1) なので、要素数が増えるほど差は顕著になる。
| 要素数 | list | set |
|---|---|---|
| 1,000 | 0.01秒 | 0.0001秒 |
| 100,000 | 1秒 | 0.0001秒 |
| 1,000,000 | 10秒 | 0.0001秒 |
存在確認が頻繁に必要なら、迷わず set を選ぶべきだ。
順序が必要な場合
要素の順序を維持する必要があるなら list を使う。
# 順序が意味を持つデータ
steps = ["mix", "bake", "cool", "decorate"]
for i, step in enumerate(steps, 1):
print(f"{i}. {step}")
set では挿入順序が保証されない(Python 3.7 以降も保証されていない)。
s = set()
s.add("a")
s.add("b")
s.add("c")
print(list(s)) # 順序は不定
インデックスアクセス
list は位置によるアクセスが可能だが、set はできない。
items = [10, 20, 30, 40, 50]
print(items[0]) # 10
print(items[-1]) # 50
print(items[1:3]) # [20, 30]
s = {10, 20, 30, 40, 50}
# s[0] # TypeError: 'set' object is not subscriptable
特定の位置にある要素を取得したいなら list が必須だ。
重複の扱い
list は同じ値を複数持てるが、set は自動的に重複を排除する。
# 重複を許容したい場合
votes = ["A", "B", "A", "A", "B", "C"]
print(votes.count("A")) # 3
# 重複を排除したい場合
unique_votes = set(votes)
print(unique_votes) # {'A', 'B', 'C'}
投票の集計など、重複が意味を持つ場合は list を使う。
メモリ使用量
同じ要素数でも、set のほうがメモリを多く消費する。
import sys
n = 1000
items_list = list(range(n))
items_set = set(range(n))
print(f"list: {sys.getsizeof(items_list):,} bytes") # 8,856 bytes
print(f"set: {sys.getsizeof(items_set):,} bytes") # 32,984 bytes
set はハッシュテーブルを維持するため、オーバーヘッドが大きい。メモリ制約がある環境では考慮が必要だ。
追加操作のパフォーマンス
末尾への追加は両者とも高速だ。
# list の append: O(1) 償却
items = []
for i in range(100000):
items.append(i)
# set の add: O(1) 平均
items = set()
for i in range(100000):
items.add(i)
ただし、list は先頭への挿入が O(n) になる。
items = list(range(10000))
# 先頭への挿入は遅い
items.insert(0, -1) # O(n)
# set には順序がないので「先頭」の概念がない
使い分けの判断基準
要素の存在確認が頻繁にある。重複を排除したい。集合演算(和集合・積集合など)を行う。
順序が重要。インデックスアクセスが必要。重複を保持したい。スライス操作を使う。
典型的な使用パターン
存在確認が必要なデータは set で持つ。
# 許可されたユーザー ID(検索が頻繁)
allowed_users = {"user123", "user456", "user789"}
def is_allowed(user_id):
return user_id in allowed_users
順序が重要なデータは list で持つ。
# 処理のキュー(順序が重要)
task_queue = ["task1", "task2", "task3"]
def process_next():
if task_queue:
return task_queue.pop(0)
両方の特性が必要なら、両方を持つこともある。
class OrderedUniqueCollection:
def __init__(self):
self._list = []
self._set = set()
def add(self, item):
if item not in self._set:
self._list.append(item)
self._set.add(item)
def __contains__(self, item):
return item in self._set # O(1)
def __iter__(self):
return iter(self._list) # 順序を保持
変換のコスト
list と set の相互変換にはコストがかかる。
import time
items = list(range(100000))
# list → set: O(n)
start = time.perf_counter()
s = set(items)
print(f"list to set: {time.perf_counter() - start:.4f}s")
# set → list: O(n)
start = time.perf_counter()
l = list(s)
print(f"set to list: {time.perf_counter() - start:.4f}s")
頻繁に変換が必要なら、最初から適切な型を選ぶほうがよい。検索が多いなら set、順序操作が多いなら list で持ち続ける設計を心がけるべきだ。