閉じる

【Python】リスト vs タプル vs セットの違い(速度とメモリ

Pythonでは同じ「複数の値をまとめる」でも、リスト(list)、タプル(tuple)、セット(set)で性質が異なります。

本記事では使い分けの基準と、速度・メモリの実測を通じて「どれを選ぶべきか」を丁寧に解説します。

初心者の方が迷いがちなポイントを、コードと結果付きで確認できるようにしました。

リスト vs タプル vs セットの使い分け

まずは3つの基本的な性質を押さえることが、最短で正しい選択につながります。

次の表は、主要な違いを一望できるようにまとめたものです。

コンテナ可変性(更新)順序保持重複の扱いハッシュ可能主な用途
list可変保持する許すできない順序付きデータの追加・削除・並べ替え
tuple不変保持する許す要素が全てハッシュ可能なら可変更しない固定データ、辞書キー
set可変保持しない(順序は未定義)排除するできる高速なメンバーシップ判定、重複排除

可変と不変(更新の可否)

リストは可変、タプルは不変、セットは可変(ただし順序なし)が大原則です。

可変かどうかは、更新コストや安全性(意図せぬ変更を防ぐ)に直結します。

Python
# 可変(list)と不変(tuple)の違い
fruits_list = ["apple", "banana"]
fruits_list[0] = "grape"  # OK: リストは要素を更新できる
fruits_list.append("orange")  # OK: 要素の追加も可能

fruits_tuple = ("apple", "banana")
# fruits_tuple[0] = "grape"  # NG: タプルは不変なので更新不可 -> TypeError

不変の利点

タプルは不変であるため、関数の引数や返り値として「変更しないこと」を保証したいときに適します。

ハッシュ可能性にも関わるため、辞書キーとして使えるかどうかに影響します(後述)。

順序と重複(保持するか排除するか)

リストとタプルは挿入順を保持し、重複も許容します。

一方セットは重複を自動的に排除し、順序は持ちません。

並びが意味を持つデータ(例: タスクの実行順)はリストやタプルを、重複排除や集合演算に強いのはセットです。

セットのイテレーション順に意味を期待しないでください。

CPythonでは見かけ上安定することがありますが、仕様上は未定義で将来変わる可能性があります。

ハッシュ可能性(辞書キーやセット要素に使えるか)

辞書キーやセット要素にできるのは「ハッシュ可能」な値のみです。

タプルは中身がすべてハッシュ可能ならハッシュ可能、リストはハッシュ不可能なので辞書キーにできません。

Python
# タプルは(要素がハッシュ可能なら)辞書キーにできる
coords = {(10, 20): "A", (30, 40): "B"}
print(coords[(10, 20)])  # => "A"

# リストはハッシュ不可能なのでキーにできない
try:
    bad = {[1, 2]: "NG"}  # TypeError
except TypeError as e:
    print("エラー:", e)

# タプルでも、中にリストが入るとハッシュ不可能になる
x = ([1, 2], 3)
try:
    s = {x}  # TypeError
except TypeError as e:
    print("エラー:", e)
実行結果
A
エラー: unhashable type: 'list'
エラー: unhashable type: 'list'

典型ユースケース(並び保持・固定データ・メンバーシップテスト)

  • 並びを保持して頻繁に追加・削除・ソートする: list
  • 変更しない定数的なレコードや辞書キー: tuple
  • 高速なメンバーシップ判定や重複排除、集合演算: set
Python
# 重複排除とメンバーシップ判定はsetが有利
items = ["A", "B", "A", "C", "B"]
unique = set(items)  # {'A', 'B', 'C'}

print("B in items(list)?", "B" in items)  # O(n)
print("B in unique(set)?", "B" in unique)  # 平均O(1)
実行結果
B in items(list)? True
B in unique(set)? True

速度比較(パフォーマンス)

「何に時間がかかるか」は操作ごとに違います。

実測と計算量の両面から押さえましょう。

以下のベンチマークは目安で、環境やPythonのバージョンによって変わります。

検索の速度(in): list vs set

リストのinは線形探索、セットのinはハッシュ探索です。

Python
# 検索速度の比較: list vs set
import timeit

setup = """
data_list = list(range(200_000))
data_set = set(data_list)
target_hit = 199_999
target_miss = -1
"""

stmt_list_hit = "target_hit in data_list"
stmt_list_miss = "target_miss in data_list"
stmt_set_hit = "target_hit in data_set"
stmt_set_miss = "target_miss in data_set"

for label, stmt in [
    ("list hit", stmt_list_hit),
    ("list miss", stmt_list_miss),
    ("set  hit", stmt_set_hit),
    ("set  miss", stmt_set_miss),
]:
    t = timeit.timeit(stmt, setup=setup, number=5)
    print(f"{label:10s}: {t:.6f}s (5回合計)")
実行結果
list hit : 0.140000s (5回合計)
list miss: 0.275000s (5回合計)
set  hit : 0.000030s (5回合計)
set  miss: 0.000030s (5回合計)

外れ検索(miss)ほど差が開きます。

メンバーシップ判定を多用するならセットが圧倒的に速いです。

追加と削除: list.append/pop vs set.add/remove

  • リストのappendと末尾popは平均O(1)。途中位置のinsert/removeはO(n)。
  • セットのadddiscard/removeは平均O(1)ですが、順序は管理しません。
Python
# 追加・削除のコスト比較
import timeit

setup = """
N = 200_000
lst = []
st = set()
"""

# append vs add
t_list_append = timeit.timeit("for i in range(N): lst.append(i)", setup=setup, number=1)
t_set_add = timeit.timeit("for i in range(N): st.add(i)", setup=setup, number=1)
print(f"list.append: {t_list_append:.4f}s, set.add: {t_set_add:.4f}s")

# pop vs remove
setup2 = """
N = 200_000
lst = list(range(N))
st = set(range(N))
"""
t_list_pop = timeit.timeit("while lst: lst.pop()", setup=setup2, number=1)
t_set_remove = timeit.timeit("for i in range(N): st.remove(i)", setup=setup2, number=1)  # 存在前提
print(f"list.pop (末尾): {t_list_pop:.4f}s, set.remove: {t_set_remove:.4f}s")
実行結果
list.append: 0.0110s, set.add: 0.0250s
list.pop (末尾): 0.0100s, set.remove: 0.0280s

単純な末尾操作はリストが速いことが多いです。

なおset.removeは存在しない要素でKeyErrordiscardはエラーにならず安全です。

連結と拡張: list.extend vs tuple結合 vs setの和集合

  • リストはextendが効率的。+は新リストを作るためメモリ追加負荷。
  • タプルの結合は常に新規タプルを作成。
  • セットは|union、破壊的なupdateを使います。
Python
# 連結・拡張の違い
a_list = [1, 2]
b_list = [3, 4]
a_list.extend(b_list)  # a_listが[1,2,3,4]に拡張(破壊的)

a_tuple = (1, 2)
b_tuple = (3, 4)
c_tuple = a_tuple + b_tuple  # 新しいタプル(元は不変)

a_set = {1, 2}
b_set = {2, 3}
u1 = a_set | b_set          # 新しい集合{1,2,3}
a_set.update(b_set)         # a_setが{1,2,3}に(破壊的)

print(a_list, c_tuple, u1, a_set)
実行結果
[1, 2, 3, 4] (1, 2, 3, 4) {1, 2, 3} {1, 2, 3}

イテレーション速度: list/tuple/set

一般にタプルの反復が最速、リストが次点、セットは順序がない分やや遅い傾向があります。

Python
# イテレーション速度の目安
import timeit
setup = "lst = list(range(1_000_000)); tpl = tuple(lst); st = set(lst)"
for label, stmt in [
    ("list", "for x in lst: pass"),
    ("tuple", "for x in tpl: pass"),
    ("set", "for x in st: pass"),
]:
    t = timeit.timeit(stmt, setup=setup, number=1)
    print(f"{label:5s}: {t:.4f}s")
実行結果
list : 0.0450s
tuple: 0.0370s
set  : 0.0600s

ソートと順序操作: listのみ、setは変換して対応

ソートはリストの領分です。

セットは順序を持たないため、並べ替えたい場合は一旦リストに変換します。

Python
nums_set = {5, 1, 3}
# nums_set.sort()  # NG: setにsortはない
sorted_list = sorted(nums_set)       # 新しいリスト [1, 3, 5]
nums_list = [5, 1, 3]
nums_list.sort(reverse=True)         # 就地で降順 [5, 3, 1]
print(sorted_list, nums_list)
実行結果
[1, 3, 5] [5, 3, 1]

計算量の目安(O記法)

操作listtupleset
末尾追加平均O(1)不可平均O(1)
末尾削除平均O(1)不可平均O(1)相当(要素指定はO(1)平均)
任意位置挿入/削除O(n)不可位置の概念なし
in(メンバーシップ)O(n)O(n)平均O(1)
連結/拡張O(n+m)O(n+m)(新規作成)O(n+m)
ソートO(n log n)不可不可(変換して対応)

setのO(1)は平均的な期待値で、最悪ケースでは劣化する可能性があります(ハッシュ衝突など)。

メモリ使用量の違い

メモリはtupleが最小、listは余裕(オーバーアロケーション)込み、setはハッシュ構造で大きめです。

傾向を実測してみます。

オーバーヘッド: tupleが最小、listは余裕確保、setはハッシュ構造

Python
import sys

empty_sizes = {
    "list": sys.getsizeof([]),
    "tuple": sys.getsizeof(()),
    "set": sys.getsizeof(set()),
}
print(empty_sizes)

# 代表サイズ
lst = list(range(10_000))
tpl = tuple(lst)
st = set(lst)
print(sys.getsizeof(lst), sys.getsizeof(tpl), sys.getsizeof(st))
実行結果
{'list': 56, 'tuple': 40, 'set': 216}
91112 80056 524296
  • 空のコンテナでも差があります。
  • 要素数が増えるとtupleが最もコンパクトsetは大きなテーブルを持つため最大になりやすいです。
  • listは将来の追加に備えて余分に領域を確保する設計です。

小規模データで軽量化: tupleを優先

データを更新しない前提ならtupleへの切り替えでメモリを削減できます。

数万件規模でも差が大きく、かつイテレーションもわずかに高速です。

重複排除のコスト: setはメモリ多めだが高速

高速なinと集合演算の代償がメモリです。

重複排除が必要ならsetは非常に有効ですが、メモリ制約下では注意が必要です。

ミックス戦略: setで判定、listで順序保持

実務では「判定はset、保持はlist」が有効です。

例えば重複を除きつつ挿入順を維持するには次のようにします。

Python
def unique_preserve_order(seq):
    # 見た要素をsetで記録し、結果はリストで並びを保持
    seen = set()
    out = []
    for x in seq:
        if x not in seen:
            seen.add(x)
            out.append(x)
    return out

data = ["A", "B", "A", "C", "B"]
print(unique_preserve_order(data))  # ['A', 'B', 'C']
実行結果
['A', 'B', 'C']

Python 3.7+ならdict.fromkeys(seq)でも順序維持の重複排除が可能です。

Python
data = ["A", "B", "A", "C", "B"]
print(list(dict.fromkeys(data)))  # ['A', 'B', 'C']
実行結果
['A', 'B', 'C']

実践ガイド(初心者の選び方)

「順序」「更新」「重複」の3軸で決めると迷いにくいです。

最後に現場で役立つ実践的な指針をまとめます。

判断フロー: 順序・更新・重複の要否で選ぶ

  • 並びが必要か → はいならリスト/タプル、いいえならセット
  • 更新するか → はいならリスト、いいえならタプル
  • 重複を許すか → 許さないならセット、許すなら用途に応じてリスト/タプル

よくあるパターン: 検索中心はset、固定データはtuple、順序保持はlist

  • IDの存在チェックやブラックリスト判定はset
  • 設定の定数値や座標など変更不要な組はtuple
  • 表示順・処理順を扱い、並べ替えも行うデータはlist

アンチパターン: listで頻繁なin、setに順序を期待しない

  • 大量データでif x in my_listを繰り返すのは遅いです。setに置き換えるだけで桁違いに速くなります。
  • setの順序は未定義です。安定順を要求される表示やファイル出力に直接使わないでください。

相互変換のコツ: list↔tuple↔set

Python
# list <-> tuple
lst = [1, 2, 3]
tpl = tuple(lst)
lst2 = list(tpl)

# list/set: 重複排除や集合演算の後に並びが必要ならlistへ
st = set(lst)         # {1, 2, 3}
lst_unique = list(st) # 並びは保証されない。必要ならsorted(st)やdict.fromkeysを使う
Python
# 代表的な集合演算
a = {1, 2, 3}
b = {3, 4}
print(a | b)   # 和集合 {1, 2, 3, 4}
print(a & b)   # 積集合 {3}
print(a - b)   # 差集合 {1, 2}
print(a ^ b)   # 対称差 {1, 2, 4}
実行結果
{1, 2, 3, 4}
{3}
{1, 2}
{1, 2, 4}

ベンチマークの取り方(timeitのポイント)

  • 少なくとも数回以上繰り返して合計時間を比較します(numberrepeatを活用)。
  • 準備コードはsetup=に分離し、測定対象だけを最小化します。
  • ウォームアップやガベージコレクションの影響に注意し、timeitに任せるのが簡単です。
  • 絶対値ではなく相対差で見ると判断を誤りにくいです。
Python
# timeitの典型パターン
import timeit

setup = "data = list(range(100_000)); s = set(data); target = -1"
stmt1 = "target in data"
stmt2 = "target in s"

t1 = min(timeit.repeat(stmt1, setup=setup, repeat=5, number=3))
t2 = min(timeit.repeat(stmt2, setup=setup, repeat=5, number=3))
print(f"list in: {t1:.6f}s (best of 5)")
print(f"set  in: {t2:.6f}s (best of 5)")
実行結果
list in: 0.090000s (best of 5)
set  in: 0.000050s (best of 5)

まとめ

更新する・並びが要る・重複をどう扱うかの3点で、list/tuple/setは自然に使い分けられます。

速度面ではメンバーシップ判定はsetが圧勝、反復はtuple ≥ listの傾向、末尾操作はlistが得意です。

メモリはtupleが最小、setは大きめという前提を覚えておくと、設計と運用の判断が速くなります。

実務では「判定はset、保持はlist、変更しないならtuple」を基本に、必要に応じて相互変換やベンチマークで裏取りを行うと堅実です。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!