2.10. 集合

集合(set) は、一意な要素の順序なしコレクションです。すでに存在する値を追加しても効果はなく、反復処理では各値がちょうど1回ずつ得られます。集合は、メンバーシップと重複排除が重要で順序が問題にならない場合に適したツールです。

2.10.1. 集合の作成

空でない集合には波かっこを使い、空の集合には set() を使います:

colours = {"red", "green", "blue"}
empty = set()

波かっこは dict リテラルのように見えます。{} 単独では空の集合ではなく空の 辞書 です。これはPythonの歴史的な事故の1つです。空の場合は set() を使ってください。

set() は任意のイテラブルからも集合を構築します。これはシーケンスから重複を除去する標準的な方法です:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

出力:

{1, 2, 3, 4}

出力順は変わることがあります。集合は特定の順序で反復処理することを保証しません。

2.10.2. 集合と辞書

集合と辞書はどちらも一意な要素をハッシュテーブルに格納します。違いは各要素が何を伴うかです:

  • dictキーと値のペア を格納します。キーを参照するとその値が返されます。

  • set要素そのものだけ を格納します。要素を参照すると、それが存在するかどうかがわかります。

両者の選択は、各要素に伴う値 に意味があるかどうかにかかっています:

  • 各要素の隣に値が属さない場合は 集合 を選びます。つまり、要素が存在するかどうかだけが重要な場合や、和集合・積集合で一意な要素のグループを組み合わせる場合です。

  • 各要素が、参照によって取得すべきデータと対になっている場合は 辞書 を選びます。設定マップ、キャッシュ、名前でキー付けされたカウンタなどです。

この2つの型は表面的な構文の多くを共有しており、それが混乱の大半の原因です。1つの表にまとめた違いは次のとおりです:

set

dict

保持するもの

一意な要素

一意なキー、それぞれに値を伴う

要素ありリテラル

{1, 2, 3}

{"a": 1, "b": 2}

空リテラル

set()

{}

メンバーシップテスト

x in s

k in d(キーのみ)

値の取得

該当なし

d[k]

要素の追加

s.add(x)

d[k] = v

反復処理

要素が得られる

キーが得られる(ペアには d.items() を使う)

要素ありリテラルと空リテラルの非対称性が、指摘しておくべき落とし穴です:

  • 要素が入った 波かっこ -- {1, 2, 3} -- は集合リテラルです。キーと値のペアが入った 波かっこ -- {"a": 1} -- は辞書リテラルです。パーサは中身によって両者を区別します。

  • 中身が何もない 波かっこ -- {} -- は空の辞書であり、空の集合ではありません。辞書のほうが先に存在したため、空リテラルは辞書のものです。空の集合には波かっこのリテラルがまったく存在せず、set() と書かなければなりません。

辞書のキーだけが常に読み取られる場合の一般的なパターンは、集合に切り替えることです。これにより意図が明確になり、使われない値をメモリから削ぎ落とせます。

2.10.3. 追加と削除

  • set.add() -- 1つの要素を挿入します。

  • set.discard() -- 要素が存在すれば削除し、存在しなければ何もしません。

  • set.remove() -- 要素を削除します。存在しない場合は KeyError を送出します。

  • set.clear() -- 集合を空にします。

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

出力:

{1, 3, 4}

2.10.4. メンバーシップ

in 演算子はメンバーシップをテストします。集合では、サイズに関係なくほぼ定数時間です。これが、「この値がそこにあるか」だけを問いたい場合に list よりも集合を選ぶ主な理由です:

if "red" in colours:
    print("colour is allowed")

同じ内容の list であれば毎回先頭から走査します。10要素なら問題ありませんが、1万要素では遅くなります。

2.10.5. 集合演算

2つの集合は通常の数学的演算で組み合わせられます。それぞれに演算子形式とメソッド形式の両方があります:

  • a | b または a.union(b) -- どちらかの集合に含まれるすべて。

  • a & b または a.intersection(b) -- 両方に現れるものだけ。

  • a - b または a.difference(b) -- a にあって b にないもの。

  • a ^ b または a.symmetric_difference(b) -- 一方にだけあって両方にはないもの。

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

出力:

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

演算子形式は読み取り専用です。メソッド形式は右辺に別の集合だけでなく任意のイテラブルを受け取ります(a.union([5, 6]))。文脈で読みやすいほうを選んでください。

2.10.6. 集合に入れられるもの

集合の要素は ハッシュ可能 でなければなりません。これは dict キーと同じ制約です。intfloatstrboolbytes、および tuple(その内容がそれ自体ハッシュ可能な場合)はすべて使えます。listdict は使えません。追加しようとすると TypeError を送出します。

2.10.7. frozenset

通常の set はミュータブルです。add / remove / discard を呼び出すたびにオブジェクトがその場で変化します。そのミュータブル性によってハッシュ可能ではなくなるため、集合を dict のキーとして、あるいは別の集合のメンバーとして使うことは できません

frozenset はそのイミュータブルな対応物です。set と同じ参照と演算子(in|&-^)を持ちますが、add / remove はなく、変更を行うメソッドもありません。その内容が決して変化しないため、frozenset のハッシュは明確に定義されます。したがってハッシュ可能 です:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

出力:

RGB

frozenset は任意のイテラブルから構築します。空の場合は frozenset()、既存の集合のイミュータブルなスナップショットを取るには frozenset(some_set) を使います:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

これに頼る一般的な理由は2つあります:

  • 辞書のキーや集合のメンバーとして使う。 単一の値では必要なものを捉えられない場面ならどこでも、値の frozenset がそれを実現できます。「このドライバがサポートする機能の集合」「このプロファイルが使うピンの集合」などです。

  • 定数をロックする。 モジュールレベルの許可された名前の frozenset は、呼び出し側が誤って変更することはできませんが、通常の set は変更できてしまいます。構築後に読み取り専用であるべきものには frozenset を優先してください。