- Python
Python setとtupleの違い|集合の使い方とlist・dict比較表
Pythonのsetは「重複しない要素の集まり(順序なし・変更できる)」、tupleは「順序つきで変更できない並び」です。 どちらも丸や波のかっこで書くので似て見えますが、役割はまったく別です。setは「含まれているか」「共通するものは何か」を高速に調べるための型、tupleは「変わらない値の組」を1つにまとめるための型です。
この記事では、list・tuple・set・frozenset・dictの5種類を1枚の比較表にまとめたうえで、集合演算の一覧、setにlistを入れたときのエラーとその直し方、要素1つのtuple (1,) の書き方、in 判定の速度差の計測までを扱います。コードはすべてPython 3.14.7で実行し、出力をそのまま載せています(3.11.15でも実行し、差があった箇所は注記しています)。
setとtupleの違いの早見表
まずは5種類のコレクションを並べた比較表です。迷ったらこの表の「向いている用途」の列を見てください。
| 項目 | list | tuple | set | frozenset | dict |
|---|---|---|---|---|---|
| 書き方 | [1, 2] |
(1, 2) |
{1, 2} |
frozenset({1, 2}) |
{"a": 1} |
| 空の書き方 | [] |
() |
set() |
frozenset() |
{} |
| 順序 | あり | あり | なし | なし | あり(挿入順) |
| 重複 | 持てる | 持てる | 持てない | 持てない | キーは持てない |
| 変更(追加・削除) | できる | できない | できる | できない | できる |
添字 x[0] |
使える | 使える | 使えない | 使えない | キーで引く |
| ハッシュ可能(setの要素やdictのキーにできる) | できない | できる(中身もハッシュ可能なとき) | できない | できる | できない |
in の計算量(平均) |
O(n) | O(n) | O(1) | O(1) | O(1)(キー) |
集合演算 | & - ^ |
なし | なし | あり | あり | キーのビューにあり |
| 向いている用途 | 順番に並べて増減させる | 変わらない値の組、戻り値、キー | 重複を除く、所属の判定、共通部分 | 定数の集合、setの中に入れる集合 | キーから値を引く |
setとtupleだけを取り出すと、次の3点が大きな違いです。
| 観点 | set | tuple |
|---|---|---|
| 順序と重複 | 順序なし・重複なし | 順序あり・重複あり |
| 変更 | add remove で変更できる |
作ったあとは変更できない |
| 得意なこと | in 判定と集合演算が速い |
添字・アンパック・辞書のキー |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python setとtupleの違い|集合の使い方とlist・dict比較表」
setとtupleをまず動かして違いを見る
最初に、いちばん小さなコードで両者の性質を確かめます。次のコードを basic.py として保存し、python3 basic.py で実行します。
# basic.py(Python 3.14.7で実行)
tags = {"python", "sql", "python", "git"} # set:重複は1つにまとまる
print(tags, len(tags))
point = (35.68, 139.76) # tuple:順序つきの組
print(point[0], point)
print(type({}), type(set()), type(()), type((1)), type((1,)))
実行結果:
{'git', 'python', 'sql'} 3
35.68 (35.68, 139.76)
<class 'dict'> <class 'set'> <class 'tuple'> <class 'int'> <class 'tuple'>
ここから読み取れることは3つあります。
- setは重複を自動で取り除きます。
"python"を2回書いても要素数は3です。 - setの表示順は書いた順とは限りません。 上の出力はアルファベット順に見えますが、たまたまです。同じコードを3.11.15で実行すると
{'sql', 'git', 'python'}と表示されました。文字列のハッシュ値は実行のたびに変わるため、順番を当てにしない書き方にします。 {}は空のdictで、空のsetはset()です。 また(1)はただの整数で、要素1つのtupleは(1,)と書きます(後の章で詳しく扱います)。
set(集合)の使い方
setを作る
| やりたいこと | 書き方 | 結果 |
|---|---|---|
| 値を並べて作る | {1, 2, 3} |
{1, 2, 3} |
| 空のsetを作る | set() |
set() |
| listから作る(重複を除く) | set([1, 1, 2]) |
{1, 2} |
| 文字列から作る | set("hello") |
文字ごとの集合 |
| 内包表記で作る | {len(w) for w in words} |
長さの集合 |
内包表記の例です。
print({len(w) for w in ["apple", "kiwi", "melon", "fig"]})
{3, 4, 5}
"apple" と "melon" はどちらも5文字なので、5は1つにまとまります。
要素を追加・削除する
s = {1, 2, 3}
s.add(4) # 1つ追加
s.update([5, 6]) # まとめて追加(listやtupleを渡せる)
s.discard(99) # 無くてもエラーにならない
try:
s.remove(99) # 無いとKeyError
except KeyError as e:
print("KeyError:", e)
s.remove(1)
print(s)
print(s.pop(), s) # どれか1つを取り出して削除
実行結果:
KeyError: 99
{2, 3, 4, 5, 6}
2 {3, 4, 5, 6}
| メソッド | 動き | 要素が無いとき |
|---|---|---|
add(x) |
1つ追加 | ― |
update(iterable) |
まとめて追加 | ― |
remove(x) |
削除 | KeyError |
discard(x) |
削除 | 何も起きない |
pop() |
どれか1つを取り出す | 空なら KeyError |
clear() |
全部削除 | ― |
「あれば消す」なら discard、「必ずあるはず」を確かめたいなら remove と使い分けると、意図がコードに表れます。pop() はどの要素が出てくるか決まっていないので、「順番に取り出す」用途にはlistや collections.deque が向いています。
重複を取り除く(順序を保つ方法も)
setのいちばんよく使われる場面が重複の除去です。ただし、set() に通すと元の順序は保たれません。 順序を保ちたいときは dict.fromkeys() を使います。
items = ["b", "a", "c", "a", "b", "d"]
print(set(items)) # 順序は保証されない
print(list(dict.fromkeys(items))) # 最初に出た順を保つ
print(sorted(set(items))) # 並べ替えてよいならこれ
実行結果:
{'a', 'c', 'd', 'b'}
['b', 'a', 'c', 'd']
['a', 'b', 'c', 'd']
| やりたいこと | 書き方 |
|---|---|
| 重複を除く(順序は問わない) | set(items) |
| 重複を除く(出現順を保つ) | list(dict.fromkeys(items)) |
| 重複を除いて並べ替える | sorted(set(items)) |
| 重複があるか調べる | len(items) != len(set(items)) |
dictはPython 3.7以降、挿入順を保つことが言語仕様になっています。dict.fromkeys(items) は「キーだけのdict」を作るので、重複を除きつつ順序が残ります。
集合演算の一覧(| & - ^ とメソッド版)
setの本領は集合演算です。2つの名簿を例にします。
A = {"田中", "佐藤", "鈴木", "高橋"} # 月曜の参加者
B = {"鈴木", "高橋", "伊藤"} # 火曜の参加者
for name, r in [("A | B", A | B), ("A & B", A & B),
("A - B", A - B), ("B - A", B - A), ("A ^ B", A ^ B)]:
print(name, "=>", sorted(r))
実行結果(見やすいように sorted() で並べ替えています):
A | B => ['伊藤', '佐藤', '田中', '鈴木', '高橋']
A & B => ['鈴木', '高橋']
A - B => ['佐藤', '田中']
B - A => ['伊藤']
A ^ B => ['伊藤', '佐藤', '田中']
演算子とメソッドの対応表
| 意味 | 演算子 | メソッド版 | 自分自身を書き換える版 | 上の例での結果 |
|---|---|---|---|---|
| 和集合(どちらかにいる) | A | B |
A.union(B) |
A |= B / A.update(B) |
5人 |
| 積集合(両方にいる) | A & B |
A.intersection(B) |
A &= B / A.intersection_update(B) |
鈴木・高橋 |
| 差集合(Aだけにいる) | A - B |
A.difference(B) |
A -= B / A.difference_update(B) |
佐藤・田中 |
| 対称差(片方だけにいる) | A ^ B |
A.symmetric_difference(B) |
A ^= B / A.symmetric_difference_update(B) |
伊藤・佐藤・田中 |
差集合だけは左右を入れ替えると結果が変わります。 A - B は「月曜だけの人」、B - A は「火曜だけの人」です。
包含関係を調べる(issubset など)
| 意味 | 演算子 | メソッド版 |
|---|---|---|
| 部分集合か(AはBに全部含まれる) | A <= B |
A.issubset(B) |
| 真部分集合か(含まれていて、かつ等しくない) | A < B |
― |
| 上位集合か(AはBを全部含む) | A >= B |
A.issuperset(B) |
| 共通要素が1つもないか | ― | A.isdisjoint(B) |
| 同じ集合か | A == B |
― |
print({"鈴木"} <= A, {"鈴木"}.issubset(A), A.issuperset({"鈴木"}),
A >= {"鈴木"}, A.isdisjoint({"山田"}),
{"鈴木"} < {"鈴木"}, {"鈴木"} <= {"鈴木"})
True True True True True False True
最後の2つに注目してください。< は「等しい場合はFalse」、<= は「等しい場合もTrue」 です。「必要な権限をすべて持っているか」のような判定は required <= owned(または required.issubset(owned))で1行に書けます。
演算子とメソッドの違い:メソッドはlistも受け取れる
演算子版は両辺がset(またはfrozenset)である必要があります。メソッド版は、listやtupleなど反復できるものなら何でも受け取ります。
print(sorted(A.union(["山田"]))) # メソッドはlistを渡せる
try:
A | ["山田"] # 演算子はsetどうしのみ
except TypeError as e:
print("TypeError:", e)
['佐藤', '山田', '田中', '鈴木', '高橋']
TypeError: unsupported operand type(s) for |: 'set' and 'list'
| 書き方 | 相手がlistのとき | 向いている場面 |
|---|---|---|
A | B などの演算子 |
TypeError |
両方がsetだとわかっているとき。短く読みやすい |
A.union(B) などのメソッド |
そのまま動く | 相手がlistやジェネレータのとき。3つ以上もまとめて A.union(B, C) と書ける |
setにlistを入れるとTypeError、tupleなら入る
setを使い始めて最初に出会いやすいエラーがこれです。
try:
{[1, 2], [3, 4]} # listをsetの要素にする
except TypeError as e:
print("TypeError:", e)
print({(1, 2), (3, 4), (1, 2)}) # tupleなら入る(重複も除かれる)
Python 3.14.7での実行結果:
TypeError: cannot use 'list' as a set element (unhashable type: 'list')
{(1, 2), (3, 4)}
Python 3.11.15では、同じコードのエラー文は次のとおりでした。
TypeError: unhashable type: 'list'
3.14ではメッセージが「setの要素として使えない」と具体的になっています。 検索するときは unhashable type: 'list' の部分が共通なので、どちらの版でも同じ原因です。
なぜlistは入らないのか(ハッシュ可能とは)
setは内部でハッシュテーブルという仕組みを使っています。要素から「ハッシュ値」という整数を計算し、その値をもとに置き場所を決めるので、in 判定が一瞬で終わります。
この仕組みが成り立つには、要素のハッシュ値が後から変わらないことが必要です。listは中身を書き換えられるので、ハッシュ値を決められません。これを「ハッシュ可能ではない(unhashable)」と呼びます。tupleは変更できないので、ハッシュ値を計算できます。
| 型 | ハッシュ可能か | setの要素・dictのキーにできるか |
|---|---|---|
int float str bool None |
できる | できる |
tuple(中身もハッシュ可能) |
できる | できる |
frozenset |
できる | できる |
list |
できない | できない |
set |
できない | できない |
dict |
できない | できない |
実行して確かめた結果です(3.11.15の出力。hash() を直接呼んだ場合は3.14でも同じ文面です)。
TypeError: unhashable type: 'list'
TypeError: unhashable type: 'set'
TypeError: unhashable type: 'dict'
tupleの中にlistがあると、やはり入らない
「tupleなら入る」には条件があります。tupleの中身もすべてハッシュ可能である必要があります。
try:
{(1, [2, 3])} # tupleの中にlist
except TypeError as e:
print("TypeError:", e)
TypeError: cannot use 'tuple' as a set element (unhashable type: 'list')
setの中にsetを入れたいときはfrozenset
setはsetの要素にできませんが、frozenset(変更できないset)なら入ります。
try:
{{1, 2}}
except TypeError as e:
print("TypeError:", e)
print({frozenset({1, 2}), frozenset({2, 1})})
TypeError: cannot use 'set' as a set element (unhashable type: 'set')
{frozenset({1, 2})}
frozenset({1, 2}) と frozenset({2, 1}) は同じ集合なので、1つにまとまっています。「順序を問わない組」を重複なく持ちたいとき(例:2人組のペアを、どちらが先でも同じとみなす)に役立ちます。
dictのキーでも同じルール
dictのキーもハッシュ可能である必要があるので、同じことが起こります。
try:
d = {[1, 2]: "x"}
except TypeError as e:
print("TypeError:", e)
d = {(1, 2): "x", frozenset({"a"}): "y"}
print(d)
TypeError: cannot use 'list' as a dict key (unhashable type: 'list')
{(1, 2): 'x', frozenset({'a'}): 'y'}
直し方の早見表:
| エラーになる書き方 | 直した書き方 | 考え方 |
|---|---|---|
{[1, 2], [3, 4]} |
{(1, 2), (3, 4)} |
listをtupleに |
set(list_of_lists) |
{tuple(x) for x in list_of_lists} |
内側をtupleに変換してから |
{{1, 2}} |
{frozenset({1, 2})} |
内側のsetをfrozensetに |
d[[x, y]] = v |
d[(x, y)] = v |
キーをtupleに |
{(1, [2, 3])} |
{(1, (2, 3))} |
中のlistもtupleに |
tupleの使い方と、要素1つの (1,) の罠
tupleは「変更できない並び」
tp = (1, 2, 3)
try:
tp[0] = 9
except TypeError as e:
print("TypeError:", e)
try:
tp.append(4)
except AttributeError as e:
print("AttributeError:", e)
TypeError: 'tuple' object does not support item assignment
AttributeError: 'tuple' object has no attribute 'append'
代入も追加もできません。「この値の組は途中で変わらない」とコードで示せるのがtupleの価値です。
ただし、変更できないのは「tupleが指している先」だけです。中にlistが入っていれば、そのlistの中身は変えられます。
tp2 = (1, [2, 3])
tp2[1].append(4)
print(tp2)
(1, [2, 3, 4])
このようなtupleは前の章で見たとおりハッシュ可能ではないので、setの要素やdictのキーにはできません。
要素1つのtupleはカンマが必要
tupleを作っているのは丸かっこではなく、カンマです。 要素が1つのときにカンマを忘れると、ただの値になります。
a = ("admin") # ただの文字列
b = ("admin",) # 要素1つのtuple
print(type(a), type(b))
for x in a: print(x, end=" ")
print()
for x in b: print(x, end=" ")
print()
t = 1, # かっこが無くてもカンマがあればtuple
print(t)
実行結果:
<class 'str'> <class 'tuple'>
a d m i n
admin
(1,)
("admin") をforで回すと、文字列が1文字ずつ出てきます。エラーにならずに動いてしまうので、気づきにくいところです。
| 書き方 | 型 | 補足 |
|---|---|---|
() |
tuple | 空のtuple |
(1) |
int | かっこは計算の優先順位を表すだけ |
(1,) |
tuple | 要素1つのtuple |
1, |
tuple | かっこなしでも作れる |
("admin") |
str | 文字列のまま |
("admin",) |
tuple | 要素1つのtuple |
tuple(["admin"]) |
tuple | listから作る |
この罠は逆向きにも起こります。行末に余分なカンマを付けると、意図せずtupleになります。
name = "admin", # 行末のカンマに注意 → ("admin",) になる
設定値を書き写したときなどに起こりやすいので、値を表示して ('admin',) のようにかっことカンマが付いていたら、行末のカンマを確認します。
アンパックと複数の戻り値
tupleがもっとも自然に使われるのがアンパック(複数の変数への一括代入)です。
x, y = 1, 2
x, y = y, x # 入れ替え
print(x, y)
first, *rest = (1, 2, 3, 4) # 先頭と残り
print(first, rest)
def minmax(v):
return min(v), max(v) # tupleを返している
lo, hi = minmax([3, 1, 4])
print(lo, hi, minmax([3, 1, 4]))
2 1
1 [2, 3, 4]
1 4 (1, 4)
return min(v), max(v) は、カンマで区切った時点でtupleになっています。関数から複数の値を返す仕組みは、実はtupleを1つ返しているだけです。*rest で受けた側はlistになる点も覚えておくと便利です。
tupleをdictのキーにする
2つ以上の値の組み合わせで値を引きたいとき、tupleをキーにすると素直に書けます。
stock = {("A棟", 1): 12, ("A棟", 2): 5, ("B棟", 1): 0}
print(stock[("A棟", 2)]) # 5
print(("B棟", 2) in stock) # False
文字列を連結して "A棟-2" のようなキーを作る方法もありますが、tupleなら区切り文字の扱いを考えずに済み、あとで分解するのも簡単です。
frozensetの使い方
frozensetは「変更できないset」です。setとの関係は、listとtupleの関係に似ています。
fs = frozenset({1, 2})
try:
fs.add(3)
except AttributeError as e:
print("AttributeError:", e)
print(fs | {3}, type(fs | {3}), type({3} | fs))
AttributeError: 'frozenset' object has no attribute 'add'
frozenset({1, 2, 3}) <class 'frozenset'> <class 'set'>
| 項目 | set | frozenset |
|---|---|---|
add remove update |
使える | 使えない |
集合演算 | & - ^ |
使える | 使える(新しい集合を返す) |
in 判定 |
O(1) | O(1) |
| setの要素・dictのキー | できない | できる |
| リテラル | {1, 2} |
なし(frozenset(...) で作る) |
setとfrozensetを混ぜて演算すると、結果の型は左側の型になります。 上の出力で fs | {3} はfrozenset、{3} | fs はsetになっています。
frozensetが向いている場面:
- モジュールの定数として持つ集合(例:
ALLOWED_EXT = frozenset({"png", "jpg", "gif"}))。うっかりaddされる心配がなくなります。 - setの中に集合を入れたいとき、または集合をdictのキーにしたいとき。
- 関数の既定値にしたいとき。変更できないので、既定値を共有しても影響が広がりません。
in判定の速度差を計測する
比較表の「in の計算量」を、実際に測って確かめます。要素数を100・1万・100万と変え、いちばん見つかりにくい末尾の要素を探す時間を測りました。
# speed.py(Python 3.14.7で実行)
import timeit
for N in (100, 10_000, 1_000_000):
data = list(range(N))
target = N - 1 # 末尾の要素を探す
cs = {"list": data, "tuple": tuple(data), "set": set(data),
"frozenset": frozenset(data), "dict": dict.fromkeys(data)}
num = 200 if N >= 1_000_000 else 20000
row = []
for k, c in cs.items():
tm = min(timeit.repeat(lambda: target in c, number=num, repeat=3)) / num
row.append(f"{k}={tm*1e6:.3f}us")
print(N, " ".join(row))
実行結果(1回あたりの時間。us はマイクロ秒=100万分の1秒):
100 list=2.236us tuple=2.161us set=0.028us frozenset=0.028us dict=0.028us
10000 list=281.215us tuple=242.377us set=0.038us frozenset=0.037us dict=0.039us
1000000 list=19856.027us tuple=14585.661us set=0.036us frozenset=0.035us dict=0.038us
表にまとめます。
| 要素数 | list | tuple | set | frozenset | dict |
|---|---|---|---|---|---|
| 100 | 2.2 us | 2.2 us | 0.028 us | 0.028 us | 0.028 us |
| 10,000 | 281 us | 242 us | 0.038 us | 0.037 us | 0.039 us |
| 1,000,000 | 19,856 us | 14,586 us | 0.036 us | 0.035 us | 0.038 us |
- listとtupleは、要素数に比例して時間が延びています(O(n))。先頭から1つずつ比べていくためです。
- set・frozenset・dictは、要素数が1万倍になってもほぼ同じ時間です(O(1))。ハッシュ値から置き場所を直接求めるためです。
- listとtupleの
inに大きな差はありません。 「tupleだから速い」わけではなく、探し方が同じだからです。
数値は共有マシンで測ったもので、環境や混み具合によって変わります(同じコードを3.11.15で測ったときは、100万件のlistで約5,000〜17,000 usの幅がありました)。絶対値よりも「listは件数に比例し、setは一定」という傾きの違いを見てください。
ループの中のinをsetに変える
速度差がもっとも効くのは、ループの中で in を繰り返すときです。2万件のIDどうしの共通部分を求める例で比べます。
import timeit
ids_a = list(range(0, 20000))
ids_b = list(range(10000, 30000))
# 直す前:listに対してinを2万回
t1 = timeit.timeit(lambda: [i for i in ids_a if i in ids_b], number=1)
# 直した後1:片方をsetにしてからin
sb = set(ids_b)
t2 = timeit.timeit(lambda: [i for i in ids_a if i in sb], number=1)
# 直した後2:集合演算で一度に求める
t3 = timeit.timeit(lambda: set(ids_a) & set(ids_b), number=1)
print(f"list-in={t1*1000:.1f}ms set-in={t2*1000:.2f}ms set&={t3*1000:.2f}ms")
実行結果(Python 3.14.7):
list-in=4167.5ms set-in=0.42ms set&=1.36ms
| 書き方 | 時間 | 特徴 |
|---|---|---|
[i for i in ids_a if i in ids_b](listのまま) |
約4,168 ms | 2万×最大2万回の比較 |
[i for i in ids_a if i in sb](片方をset化) |
約0.42 ms | 元の順序を保てる |
set(ids_a) & set(ids_b) |
約1.36 ms | 1行で書ける。順序は保たれない |
sb = set(ids_b) の1行を足すだけで、約4秒が1ミリ秒未満になりました。 順序を保ちたいなら2番目、順序を問わないなら3番目の書き方が向いています。
メモリ使用量も確認する
速さと引き換えに、setとdictはメモリを多めに使います。要素1,000個のコンテナ本体のサイズ(sys.getsizeof、単位はバイト)を測りました。
| list | tuple | set | frozenset | dict |
|---|---|---|---|---|
| 8,056 | 8,048 | 32,984 | 32,984 | 36,952 |
setはlistの約4倍です。数十件を1回だけ調べる程度ならlistやtupleのままで十分で、件数が多い、または in を何度も繰り返すときにsetへ変える、という判断がちょうどよい目安です。
どれを選ぶかの判断表
| やりたいこと | 選ぶ型 | 理由 |
|---|---|---|
| 順番に並べて、追加・削除・並べ替えをしたい | list | 順序つきで変更できる |
| 変わらない値の組を持ちたい(座標、年月日、範囲) | tuple | 変更できないことを示せる |
| 関数から複数の値を返したい | tuple | return a, b と書ける |
| dictのキーに複数の値の組を使いたい | tuple | ハッシュ可能 |
| 重複を取り除きたい | set | 自動で1つにまとまる |
| 含まれているかを何度も調べたい | set | in がO(1) |
| 2つの集まりの共通・差分を求めたい | set | & - で1行 |
| 変更されない定数の集合を持ちたい | frozenset | add できない |
| setの中に集合を入れたい | frozenset | ハッシュ可能 |
| キーから値を引きたい | dict | キーの in もO(1) |
| 出現回数を数えたい | collections.Counter |
dictの仲間。数える処理が1行 |
判断の流れを図にすると次のようになります。
flowchart TD
A[値の集まりを持ちたい] --> B{キーから値を引く?}
B -- はい --> D[dict]
B -- いいえ --> C{順序や重複に意味がある?}
C -- ある --> E{あとで変更する?}
E -- する --> F[list]
E -- しない --> G[tuple]
C -- ない --> H{あとで変更する?}
H -- する --> I[set]
H -- しない --> J[frozenset]
namedtuple・dataclassとの関係
tupleは便利ですが、row[0] row[1] のような添字アクセスは、何番目が何の値かを覚えておく必要があります。項目に名前を付けたいときの選択肢が、namedtupleとdataclassです。
from collections import namedtuple
from dataclasses import dataclass
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
print(p, p.x, p[0], p == (1, 2), {p, Point(1, 2)})
@dataclass
class P1:
x: int
y: int
@dataclass(frozen=True)
class P2:
x: int
y: int
try:
{P1(1, 2)} # 通常のdataclassはsetに入らない
except TypeError as e:
print("TypeError:", e)
print({P2(1, 2), P2(1, 2)}) # frozen=Trueなら入る
try:
P2(1, 2).x = 5
except Exception as e:
print(type(e).__name__ + ":", e)
実行結果(Python 3.14.7):
Point(x=1, y=2) 1 1 True {Point(x=1, y=2)}
TypeError: cannot use 'P1' as a set element (unhashable type: 'P1')
{P2(x=1, y=2)}
FrozenInstanceError: cannot assign to field 'x'
| 項目 | tuple | namedtuple | dataclass | dataclass(frozen=True) |
|---|---|---|---|---|
名前でアクセス(p.x) |
できない | できる | できる | できる |
添字でアクセス(p[0]) |
できる | できる | できない | できない |
| 変更 | できない | できない | できる | できない |
| setの要素・dictのキー | できる | できる | できない | できる |
(1, 2) と == で等しい |
― | 等しい | 等しくない | 等しくない |
| 型ヒント・既定値・メソッド | ― | 一部 | 書きやすい | 書きやすい |
- namedtupleは「名前つきのtuple」 です。tupleの性質(変更できない、ハッシュ可能、アンパックできる)をそのまま引き継ぎます。
- dataclassは通常のクラスです。
==を自動で定義する代わりにハッシュは無効になるため、そのままではsetに入りません。frozen=Trueを付けると変更できなくなり、setの要素やdictのキーに使えます。
「setに入れたい」「dictのキーにしたい」という要件があるなら、tuple・namedtuple・dataclass(frozen=True) のいずれかを選ぶ、と覚えておくと迷いません。namedtupleやCounterなどの詳細は「Python collectionsの使い方」、dataclassの既定値やslotsは「Python dataclassの使い方」、listとdictのメソッドは「Python リスト 操作 一覧」「Python辞書(dict)の使い方」の各記事で扱っています。
つまずきやすいポイントのチェックリスト
| 症状 | 原因 | 直し方 |
|---|---|---|
TypeError: unhashable type: 'list' |
setの要素やdictのキーにlistを使った | tupleに変換する |
TypeError: 'set' object is not subscriptable |
s[0] と添字でアクセスした |
list(s)[0] か sorted(s)[0]、または next(iter(s)) |
空のsetのつもりが dict になる |
{} と書いた |
set() と書く |
| forで1文字ずつ出てくる | ("admin") とカンマなしで書いた |
("admin",) と書く |
値が ('admin',) になる |
行末に余分なカンマがある | カンマを消す |
| setの順序が実行のたびに変わる | setは順序を持たない | sorted(s) で並べる、順序が要るならlistやdictを使う |
A | [1, 2] で TypeError |
演算子はsetどうしのみ | A.union([1, 2]) か A | set([1, 2]) |
{1, 1.0, True} が {1} になる |
1 == 1.0 == True でハッシュ値も同じ |
型を区別したいなら (type(x), x) のtupleで持つ |
| tupleの中のlistが変わっている | tupleは「参照先」を固定するだけ | 中身もtupleにする |
最後から2番目の行は意外に感じるかもしれません。実行すると次のようになります。
print(1 in {1.0, True}, {1, 1.0, True}, {0, False, 0.0})
True {1} {0}
Pythonでは 1 == 1.0 == True が成り立ち、ハッシュ値も同じなので、setの中では同じ要素として扱われます。最初に入れた値が残ります。
よくある質問
Q. setとtupleはどちらが速いですか?
A. 用途によります。in で含まれるかを調べるならsetが圧倒的に速く、今回の計測では100万件でtupleが約14,586マイクロ秒、setが約0.036マイクロ秒でした。一方、先頭から順に取り出すだけならtupleで十分で、メモリもsetの約4分の1で済みます。「探す」ならset、「並べて持つ」ならtupleと考えてください。
Q. listとtupleはどう使い分ければよいですか? A. あとで追加・削除・並べ替えをするならlist、作ったあと変えない値の組ならtupleです。tupleにするとdictのキーやsetの要素に使えるようになり、「この値は変わらない」という意図も伝わります。迷ったら「同じ種類のものを並べる=list」「違う役割の値を組にする=tuple」と考えると選びやすくなります。
Q. setの要素を順番どおりに取り出すにはどうしますか?
A. setは順序を持たないので、取り出すときに sorted(s) で並べ替えます。入れた順を保ちたい場合は、setの代わりに dict.fromkeys(items) でキーだけのdictを作ると、重複を除きつつ挿入順が残ります。
Q. frozensetはどんなときに使いますか?
A. 「変更されない集合」が必要なときです。モジュールの定数にする、setの中に集合を入れる、集合をdictのキーにする、関数の既定値にする、といった場面に向いています。集合演算や in の速さはsetと同じです。
まとめ
- setは「順序なし・重複なし・変更できる」、tupleは「順序あり・重複あり・変更できない」。 役割が違うので、用途で選びます。
in判定はset・frozenset・dictがO(1)、list・tupleがO(n)。 100万件の計測で数十万倍の差が出ました。- 集合演算は
| & - ^、メソッド版はunionintersectiondifferencesymmetric_difference。 メソッド版はlistも受け取れます。 - setの要素とdictのキーはハッシュ可能なものだけ。 listはtupleに、setはfrozensetに変えると入ります。
- 要素1つのtupleは
(1,)。 tupleを作るのはかっこではなくカンマです。 - 空のsetは
set()。{}は空のdictです。 - 名前つきで持ちたいならnamedtupleか
dataclass(frozen=True)。 どちらもsetの要素にできます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python setとtupleの違い|集合の使い方とlist・dict比較表」
株式会社RJCでは、Pythonのsetやtupleのようなデータ構造の選び方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。