Python の set vs list(いつ集合を使うべきか)

set と list は Python でよく使われるコレクション型だが、特性が大きく異なる。適切に使い分けることで、パフォーマンスとコードの明確さが向上する。

基本的な特性の違い

両者の根本的な違いを整理する。

list

順序あり、重複あり、インデックスアクセス可能、ミュータブル

set

順序なし、重複なし、インデックスアクセス不可、ミュータブル

この違いがすべての判断基準になる。

検索パフォーマンス

要素の存在確認は、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) なので、要素数が増えるほど差は顕著になる。

要素数listset
1,0000.01秒0.0001秒
100,0001秒0.0001秒
1,000,00010秒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 を選ぶ場面

要素の存在確認が頻繁にある。重複を排除したい。集合演算(和集合・積集合など)を行う。

list を選ぶ場面

順序が重要。インデックスアクセスが必要。重複を保持したい。スライス操作を使う。

典型的な使用パターン

存在確認が必要なデータは 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 で持ち続ける設計を心がけるべきだ。