17. セット(set): 一意で順序のないデータを扱う
これまでの章では、リスト(list)(順序付きで可変なコレクション)や辞書(dictionary)(キーと値のマッピング)を扱ってきました。ここでは セット(set) を取り上げます。セットは、一意な要素を保存し、数学的な集合演算を効率よく行うために設計された Python のコレクション型です。
セットは、重複を取り除きたい場合、所属判定を素早く行いたい場合、またはコレクション間で共通要素を探すといった操作を行いたい場合に特に強力です。リストと違い、セットは順序を持たず、重複する値を含められません。同じ要素を 2 回追加しようとしても何も起きません。
17.1) セットの作成と基本操作
17.1.1) 波括弧でセットを作成する
セットを作成する最も一般的な方法は、波括弧 {} にカンマ区切りで値を並べる方法です。
# プログラミング言語のセットを作成する
languages = {"Python", "JavaScript", "Java", "C++"}
print(languages) # Output: {'Python', 'JavaScript', 'Java', 'C++'}
print(type(languages)) # Output: <class 'set'>重要: セットを print したときの要素の順序は、入力した順序と異なることがあります。セットは 順序のないコレクション であり、Python は特定の並び順を保持しないためです。
numbers = {5, 2, 8, 1, 9}
print(numbers) # Output might be: {1, 2, 5, 8, 9} or another order出力順は Python の実行ごとやバージョンによって変わる可能性があります。セットが特定の順序を保つことを前提にしてはいけません。順序が重要なら、代わりにリストを使ってください。
17.1.2) セットは自動的に重複を取り除く
セットの最も便利な性質の 1 つは、重複値を自動的に取り除くことです。重複する要素を含めてセットを作ろうとしても、それぞれの一意な値は 1 つだけが保持されます。
# 重複値を含むセットを作成する
student_ids = {101, 102, 103, 102, 101, 104}
print(student_ids) # Output: {101, 102, 103, 104}
# この性質により、セットは重複除去に最適です
grades = [85, 90, 85, 78, 90, 92, 78, 85]
unique_grades = set(grades)
print(unique_grades) # Output: {78, 85, 90, 92}この自動的な重複排除は、セットが数学的な集合モデルを使っており、各要素は 1 回しか現れないために起こります。すでに存在する値を追加すると、セットはその重複を単に無視します。
17.1.3) set() コンストラクタでセットを作成する
set() コンストラクタを使うと、他の反復可能(iterable)なオブジェクトからセットを作成できます。これは、リスト、タプル、文字列をセットに変換するのに特に便利です。
# リストからセットを作成する
colors_list = ["red", "blue", "green", "red", "yellow"]
colors_set = set(colors_list)
print(colors_set) # Output: {'red', 'blue', 'green', 'yellow'}
# 文字列からセットを作成する(各文字が要素になる)
letters = set("programming")
print(letters) # Output: {'p', 'r', 'o', 'g', 'a', 'm', 'i', 'n'}
# タプルからセットを作成する
coordinates = set((10, 20, 30, 20, 10))
print(coordinates) # Output: {10, 20, 30}文字列からセットを作成すると、一意な各文字がそれぞれ別要素になります。これは、テキスト内の異なる文字をすべて見つけたい場合に便利です。
text = "Mississippi"
unique_chars = set(text.lower())
print(unique_chars) # Output: {'m', 'i', 's', 'p'}
print(f"The word contains {len(unique_chars)} unique letters")
# Output: The word contains 4 unique letters17.1.4) 空のセットを作成する
ここには重要な落とし穴があります。{} で空のセットは作れません。Python はそれを空の辞書として解釈します。代わりに set() を使う必要があります。
# WRONG - これはセットではなく空の辞書を作成します
empty_dict = {}
print(type(empty_dict)) # Output: <class 'dict'>
# CORRECT - これは空のセットを作成します
empty_set = set()
print(type(empty_set)) # Output: <class 'set'>
print(empty_set) # Output: set()この区別があるのは、セットより前に辞書が Python に追加され、{} がすでに空の辞書の表記として使われていたためです。空のセットを表示すると、混乱を避けるために Python は set() と表示します。
初心者によくある混乱: 変数を使って要素 1 つのセットを作る場合、そのセットに入るのは変数名ではなく変数の 値 です。
# 変数を使ったセット作成の理解
x = 5
my_set = {x} # {'x'} ではなく {5} を作成します
print(my_set) # Output: {5}
# 文字列 'x' を含むセットが欲しい場合:
my_set = {'x'}
print(my_set) # Output: {'x'}
# これは任意の式にも当てはまります
result = 10 + 5
my_set = {result} # {15} を作成します
print(my_set) # Output: {15}17.1.5) セットの基本的な性質と操作
セットはいくつかの基本操作をサポートしており、データ処理に役立ちます。
# 一意な要素数を確認する
website_visitors = {"alice", "bob", "charlie", "alice", "david"}
print(f"Unique visitors: {len(website_visitors)}")
# Output: Unique visitors: 4
# 'in' で所属判定(セットではとても高速)
if "alice" in website_visitors:
print("Alice visited the website")
# Output: Alice visited the website
# 非所属判定
if "eve" not in website_visitors:
print("Eve has not visited yet")
# Output: Eve has not visited yetin による所属判定は、セットの主要な利点の 1 つです。大きなコレクションでは、セットに要素が存在するかどうかの確認は、リストで確認するよりもずっと高速です。なぜこれが重要なのかは、セクション 17.5 で確認します。
17.2) セットへの要素追加と削除
タプル(tuple)(不変)とは異なり、セットは可変(mutable)で、作成後に要素を追加したり削除したりできます。ただし、要素そのものは不変型である必要があります(この制約はセクション 17.7 で確認します)。
17.2.1) add() で要素を 1 つ追加する
個別の要素をセットに追加するのは、add() メソッドで簡単に行えます。要素がすでに存在していてもセットは変化しません。エラーは起きず、重複も作られません。
# 完了したタスクのセットを構築する
completed_tasks = {"task1", "task2"}
print(completed_tasks) # Output: {'task1', 'task2'}
# 新しいタスクを追加する
completed_tasks.add("task3")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}
# 重複を追加しても効果はありません
completed_tasks.add("task1")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}この挙動により、セットは一意な出現を追跡するのに理想的です。要素がすでに存在するかを確認しなくても安全に add() を呼び出せます。重複はセットが自動的に処理します。
17.2.2) update() で複数要素を追加する
複数の要素を一度に追加するには、update() を使います。update() は任意の反復可能オブジェクト(リスト、タプル、別のセットなど)を受け取り、その全要素をセットに追加します。
# 少数のスキルセットから開始する
skills = {"Python", "SQL"}
print(skills) # Output: {'Python', 'SQL'}
# リストから複数のスキルを追加する
new_skills = ["JavaScript", "Docker", "Python"]
skills.update(new_skills)
print(skills) # Output: {'Python', 'SQL', 'JavaScript', 'Docker'}"Python" は元のセットにも追加するリストにも出てきますが、セットには 1 つしか残りません。update() メソッドは複数の iterable を引数として受け取ることもできます。
# 複数の情報源からスキルを統合する
current_skills = {"Python"}
course_skills = ["JavaScript", "HTML"]
job_requirements = {"SQL", "Python", "Docker"}
current_skills.update(course_skills, job_requirements)
print(current_skills)
# Output: {'Python', 'JavaScript', 'HTML', 'SQL', 'Docker'}17.2.3) remove() で要素を削除する
要素の削除には注意が必要です。remove() メソッドはセットから要素を削除しますが、その要素が存在しない場合は KeyError を送出します。
# アクティブユーザーを管理する
active_users = {"alice", "bob", "charlie", "david"}
# ログアウトしたユーザーを削除する
active_users.remove("bob")
print(active_users) # Output: {'alice', 'charlie', 'david'}
# 存在しない要素を削除しようとするとエラーになる
# active_users.remove("eve") # Raises: KeyError: 'eve'remove() は要素がないとエラーになるため、要素が存在すると確信している場合や、存在しないことをエラーとして捕捉したい場合に使うのがよいでしょう。
# エラーハンドリングによる安全な削除(try/except については第 28 章で詳しく学びます)
users = {"alice", "bob", "charlie"}
user_to_remove = "david"
if user_to_remove in users:
users.remove(user_to_remove)
print(f"Removed {user_to_remove}")
else:
print(f"{user_to_remove} was not in the set")
# Output: david was not in the set17.2.4) discard() で安全に要素を削除する
エラーを起こさずに安全に削除したい場合は、discard() が寛容な代替手段になります。要素があれば削除しますが、なければ何もしません。
# ショッピングカートを管理する
cart_items = {"apple", "banana", "orange"}
# 安全に削除する(要素がなくてもエラーにならない)
cart_items.discard("banana")
print(cart_items) # Output: {'apple', 'orange'}
cart_items.discard("grape") # grape がセットに無くてもエラーにならない
print(cart_items) # Output: {'apple', 'orange'}discard() は、最初から要素があったかどうかに関わらず「その要素がセットに含まれない状態にしたい」場合に使います。要素がないことをエラー条件として扱って捕捉したい場合は remove() を使います。
17.2.5) pop() で任意の要素を削除して返す
pop() メソッドはセットから任意の要素を削除し、その要素を返します。セットは順序を持たないため、どの要素が削除されるかは予測できません。
# 保留中タスクのキューを処理する(順序は重要ではない)
pending_tasks = {"email", "report", "meeting", "review"}
# タスクを 1 つ処理する(どれでもよい)
task = pending_tasks.pop()
print(f"Processing: {task}") # Output: Processing: email (or another task)
print(f"Remaining: {pending_tasks}")
# Output: Remaining: {'report', 'meeting', 'review'} (without the popped task)空のセットに対して pop() を呼ぶと KeyError を送出します。
empty_set = set()
# empty_set.pop() # Raises: KeyError: 'pop from an empty set'pop() は、セット内の全要素を処理したいが順序は気にしない、という場合に便利です。
# セット内の全要素を処理する
items_to_process = {"item1", "item2", "item3"}
while items_to_process:
item = items_to_process.pop()
print(f"Processing {item}")
# Process the item...
print("All items processed")
# Output:
# Processing item1
# Processing item2
# Processing item3
# All items processed17.2.6) clear() で全要素を削除する
clear() メソッドは、セットからすべての要素を削除して空にします。
# セッションデータをリセットする
session_data = {"user_id", "timestamp", "ip_address"}
print(session_data) # Output: {'user_id', 'timestamp', 'ip_address'}
session_data.clear()
print(session_data) # Output: set()
print(len(session_data)) # Output: 0同じセットオブジェクトを再利用したい場合は、新しい空のセットを作るよりも効率的です。
17.3) 集合演算: 和集合・積集合・差集合・対称差
セットは数学的な集合演算をサポートしており、コレクションの結合、比較、分析を効率よく行えます。これらの演算は集合論の基本であり、データ処理で多くの実用的な用途があります。
17.3.1) 和集合: セットを結合する
和集合がなぜ重要なのかを理解するために、実用的なシナリオから始めましょう。たとえば、異なるコースにまたがる学生の履修状況を管理しているとします。
# 異なるコースに履修している学生
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# どちらか(または両方)のコースを取っている全学生を見つける
all_students = python_students | javascript_students
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}2 つのセットの 和集合(union) には、どちらか一方(または両方)に現れるすべての要素が含まれます。Python には和集合を計算する方法が 2 つあります。上で示した | 演算子と、union() メソッドです。
# union() メソッドでも同じ結果
all_students = python_students.union(javascript_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}union() メソッドは複数のセットを引数に取れるため、多くの情報源からデータをまとめるのに便利です。
# 3 つの異なるコースの学生
python_students = {"alice", "bob"}
javascript_students = {"bob", "charlie"}
sql_students = {"charlie", "david"}
# 全コースの学生
all_students = python_students.union(javascript_students, sql_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david'}和集合の別例として、部門ごとのメールリストを結合するケースがあります。
# 異なる部門のメールリストを結合する
marketing_contacts = {"alice@company.com", "bob@company.com"}
sales_contacts = {"bob@company.com", "charlie@company.com"}
support_contacts = {"david@company.com", "alice@company.com"}
# 部門をまたぐ一意な連絡先の全体
all_contacts = marketing_contacts | sales_contacts | support_contacts
print(f"Total unique contacts: {len(all_contacts)}")
# Output: Total unique contacts: 417.3.2) 積集合: 共通要素を見つける
複数のセットに共通して現れる要素を理解することは、多くのデータ分析タスクで重要です。積集合(intersection) は、「これらのセットに共通するものは何か?」という問いに答えます。
# 両方の商品を購入した顧客を見つける
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# 両方の商品を買った顧客
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}積集合には両方のセットに現れる要素だけが含まれます。intersection() メソッドも使えます。こちらは複数のセットを受け取れます。
# 3 コースすべてを履修している学生を見つける
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "charlie", "david"}
sql_students = {"charlie", "eve", "bob"}
# 3 コースすべてを取っている学生
all_three = python_students.intersection(javascript_students, sql_students)
print(all_three) # Output: {'bob', 'charlie'}複数の倉庫で在庫がある商品を探す実用例です。
# 複数の倉庫で在庫がある商品を見つける
warehouse_a = {"laptop", "mouse", "keyboard", "monitor"}
warehouse_b = {"mouse", "keyboard", "printer", "scanner"}
warehouse_c = {"keyboard", "monitor", "mouse", "desk"}
# すべての倉庫で入手可能な商品
available_everywhere = warehouse_a & warehouse_b & warehouse_c
print(f"Available in all locations: {available_everywhere}")
# Output: Available in all locations: {'mouse', 'keyboard'}17.3.3) 差集合: 片方にあってもう片方にない要素を見つける
あるコレクションに固有のものを特定したい場合があります。差集合(difference) は、1 つ目のセットにあって 2 つ目のセットにない要素を見つけます。
# 在庫管理: 不一致を見つける
expected_items = {"item001", "item002", "item003", "item004"}
actual_items = {"item001", "item003", "item005"}
# 在庫から不足しているアイテム
missing = expected_items - actual_items
print(f"Missing items: {missing}")
# Output: Missing items: {'item002', 'item004'}
# 予期しない在庫アイテム
unexpected = actual_items - expected_items
print(f"Unexpected items: {unexpected}")
# Output: Unexpected items: {'item005'}difference() メソッドも使えます。
# Python コースのみにいる学生(JavaScript にはいない)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
python_only = python_students.difference(javascript_students)
print(python_only) # Output: {'alice', 'charlie'}重要: 差集合は 可換ではありません。つまり順序が重要です。
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# Python にいて JavaScript にはいない学生
python_only = python_students - javascript_students
print(f"Python only: {python_only}")
# Output: Python only: {'alice', 'charlie'}
# JavaScript にいて Python にはいない学生
javascript_only = javascript_students - python_students
print(f"JavaScript only: {javascript_only}")
# Output: JavaScript only: {'david', 'eve'}17.3.4) 対称差: どちらか片方にだけある要素
対称差(symmetric difference) は、どちらか片方のセットにあるが両方にはない要素を見つけます。この操作は、2 つのバージョン間の変更点を特定するのに特に便利です。
# 設定の 2 バージョンを比較する
old_settings = {"debug", "logging", "cache", "compression"}
new_settings = {"logging", "cache", "monitoring", "security"}
# 変更された設定(追加または削除)
changes = old_settings ^ new_settings
print(f"Changed settings: {changes}")
# Output: Changed settings: {'debug', 'compression', 'monitoring', 'security'}
# 何が追加されたか・削除されたかを個別に見るには:
removed = old_settings - new_settings
added = new_settings - old_settings
print(f"Removed: {removed}") # Output: Removed: {'debug', 'compression'}
print(f"Added: {added}") # Output: Added: {'monitoring', 'security'}symmetric_difference() メソッドも使えます。
# 片方のコースにだけいる学生(両方ではない)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
one_course_only = python_students.symmetric_difference(javascript_students)
print(one_course_only)
# Output: {'alice', 'charlie', 'david', 'eve'}差集合と異なり、対称差は 可換です。順序は関係ありません。
result1 = python_students ^ javascript_students
result2 = javascript_students ^ python_students
print(result1 == result2) # Output: True17.4) 部分集合と上位集合の関係(issubset, issuperset, isdisjoint)
セットを結合するだけでなく、セット同士の関係を理解する必要がよくあります。Python には、あるセットが別のセットに含まれているか、含んでいるか、あるいは共通要素がないかをテストするメソッドがあります。
17.4.1) issubset() と <= で部分集合をテストする
セット A がセット B の 部分集合(subset) であるとは、A のすべての要素が B にも含まれていることを意味します。言い換えると、B は A の全要素(そして場合によってはそれ以上)を含みます。
# コースの前提条件
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# 基本スキルが中級スキルの部分集合か確認する
print(basic_skills.issubset(intermediate_skills)) # Output: True
print(basic_skills <= intermediate_skills) # Output: True (same result)セットは常に自分自身の部分集合です。
skills = {"Python", "SQL", "JavaScript"}
print(skills.issubset(skills)) # Output: True
print(skills <= skills) # Output: True真部分集合(proper subset)(A が B の部分集合だが B と等しくはない)をテストしたい場合は、< 演算子を使います。
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# 真部分集合: basic は intermediate の部分集合で、かつ等しくない
print(basic_skills < intermediate_skills) # Output: True
# 自分自身の真部分集合ではない(等しいため)
print(basic_skills < basic_skills) # Output: False部分集合判定の実用例として、権限や要件のチェックがあります。
# ユーザー権限システム
required_permissions = {"read", "write"}
user_permissions = {"read", "write", "delete", "admin"}
# ユーザーが必要な権限をすべて持っているか確認する
if required_permissions.issubset(user_permissions):
print("Access granted")
else:
print("Access denied - missing permissions")
# Output: Access granted
# 権限が不足している別ユーザー
limited_user = {"read"}
if required_permissions.issubset(limited_user):
print("Access granted")
else:
missing = required_permissions - limited_user
print(f"Access denied - missing: {missing}")
# Output: Access denied - missing: {'write'}17.4.2) issuperset() と >= で上位集合をテストする
セット A がセット B の 上位集合(superset) であるとは、A が B の全要素を含んでいることを意味します。これは部分集合の逆関係です。A が B の部分集合なら、B は A の上位集合です。
# スキルレベル
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis", "research"}
# 上級スキルが基本スキルの上位集合か確認する
print(advanced_skills.issuperset(basic_skills)) # Output: True
print(advanced_skills >= basic_skills) # Output: True (same result)部分集合と同様に、セットは常に自分自身の上位集合です。
skills = {"Python", "SQL"}
print(skills.issuperset(skills)) # Output: True真上位集合(proper superset)(A が B の上位集合だが B と等しくはない)には、> 演算子を使います。
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis"}
# 真上位集合: advanced は basic の全要素を含み、さらに要素がある
print(advanced_skills > basic_skills) # Output: True
# 自分自身の真上位集合ではない
print(advanced_skills > advanced_skills) # Output: False17.4.3) isdisjoint() で互いに素なセットをテストする
2 つのセットが 互いに素(disjoint) であるとは、共通要素が 1 つもないこと、つまり積集合が空であることを意味します。isdisjoint() メソッドは、セット間で要素を共有しない場合に True を返します。
# スケジュールの衝突を確認する
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "art", "music"}
# 衝突があるか(両方のセッションに同じクラスがあるか)を確認する
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: No scheduling conflicts互いに素でない場合は次のようになります。
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "math", "music"}
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: Conflicts: {'math'}空のセットは、すべてのセット(空のセット同士も含む)と互いに素です。
empty = set()
numbers = {1, 2, 3}
print(empty.isdisjoint(numbers)) # Output: True
print(empty.isdisjoint(empty)) # Output: True17.5) リストの代わりにセットを使うべきとき
効率的な Python コードを書くには、セットとリストをいつ使い分けるかを理解することが重要です。どちらも要素の集合を保存しますが、特性が異なるため、用途も異なります。
17.5.1) 高速な所属判定にはセットを使う
セットの最も大きな利点の 1 つは、所属判定の速さです。要素がセットに存在するかの確認は、特に大規模なコレクションでは、リストで確認するよりもずっと高速です。
# 大規模なコレクションにユーザーが含まれるかを確認する
active_users_list = []
for i in range(10000):
active_users_list.append("user" + str(i))
# リストの場合(大規模だと遅い)
print("user5000" in active_users_list) # 見つかるまで各要素をチェックする
active_users_set = set()
for i in range(10000):
active_users_set.add("user" + str(i))
# セットの場合(サイズに関係なく高速)
print("user5000" in active_users_set) # 直接参照どちらも同じ結果になりますが、大規模なコレクションではセット版が劇的に高速です。これは、セットが内部的にハッシュテーブル(hash table)を使っており、サイズに関わらずほぼ瞬時に参照できる一方で、リストは要素を順番にチェックする必要があるためです。
17.5.2) 重複を取り除くにはセットを使う
コレクションから重複を削除したい場合、セットに変換するのが最も簡単な方法です。
# ユーザー入力から重複エントリを削除する
survey_responses = [
"yes", "no", "yes", "maybe", "yes", "no", "maybe", "yes"
]
# 一意な回答を取得する
unique_responses = set(survey_responses)
print(unique_responses) # Output: {'yes', 'no', 'maybe'}
# リストに戻したい場合(重複を削除した状態で)
unique_list = list(unique_responses)
print(unique_list) # Output: ['yes', 'no', 'maybe'] (order may vary)17.5.3) 数学的な集合演算にはセットを使う
コレクション間で共通要素、差分、和集合を求めたい場合、セットは明確で効率的な操作を提供します。
# 顧客の購買パターンを分析する
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# 両方の商品を購入した顧客
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}
# 商品 A のみを買った顧客
only_a = customers_product_a - customers_product_b
print(f"Only product A: {only_a}")
# Output: Only product A: {101, 102}
# 少なくともどちらかの商品を買った顧客
all_customers = customers_product_a | customers_product_b
print(f"Total customers: {len(all_customers)}")
# Output: Total customers: 717.5.4) 順序が重要なときはリストを使う
セットは順序を持たないため、要素の並びが重要ならリストを使う必要があります。
# WRONG - セットでは順序が保持されません
task_order = {"wake up", "breakfast", "work", "lunch", "work", "dinner"}
print(task_order) # 順序は予測できず、"work" は 1 回しか現れない
# CORRECT - 順序が重要ならリストを使います
task_order = ["wake up", "breakfast", "work", "lunch", "work", "dinner"]
print(task_order)
# Output: ['wake up', 'breakfast', 'work', 'lunch', 'work', 'dinner']17.5.5) 重複に意味があるときはリストを使う
重複値が情報(頻度や複数回の出現など)を持つなら、リストを使います。
# クイズの点数を記録する(重複が何人取ったかを示す)
quiz_scores = [85, 90, 85, 78, 90, 92, 85, 88]
# リストなら出現回数を数えられる
score_85_count = quiz_scores.count(85)
print(f"Students who scored 85: {score_85_count}")
# Output: Students who scored 85: 3
# セットだとこの情報を失う
unique_scores = set(quiz_scores)
print(unique_scores) # Output: {78, 85, 88, 90, 92}
# 何人が各点数を取ったかは分からない17.5.6) インデックス参照が必要なときはリストを使う
セットは順序を持たないため、インデックス参照をサポートしません。位置で要素にアクセスする必要があるならリストを使います。
# WRONG - セットはインデックス参照をサポートしません
colors = {"red", "blue", "green"}
# first_color = colors[0] # Raises: TypeError: 'set' object is not subscriptable
# CORRECT - インデックスアクセスにはリストを使います
colors = ["red", "blue", "green"]
first_color = colors[0]
print(first_color) # Output: red17.6) frozenset と不変なセット
ここまでは通常のセットを扱ってきました。これは可変で、作成後に要素を追加・削除できます。Python には frozenset もあり、これはセットの不変(immutable)版です。作成後は変更できません。
17.6.1) frozenset を作成する
set() で通常のセットを作るのと同様に、frozenset() コンストラクタで frozenset を作成します。
# リストから frozenset を作成する
colors = frozenset(["red", "blue", "green"])
print(colors) # Output: frozenset({'red', 'blue', 'green'})
print(type(colors)) # Output: <class 'frozenset'>
# タプルから frozenset を作成する
numbers = frozenset((1, 2, 3, 4, 5))
print(numbers) # Output: frozenset({1, 2, 3, 4, 5})
# 空の frozenset を作成する
empty = frozenset()
print(empty) # Output: frozenset()通常のセットと同様に、frozenset も重複を自動的に取り除きます。
# 重複は取り除かれる
values = frozenset([1, 2, 2, 3, 3, 3, 4])
print(values) # Output: frozenset({1, 2, 3, 4})17.6.2) frozenset は不変である
一度作成すると frozenset は変更できません。add()、remove()、discard()、pop()、clear() のようなメソッドは frozenset には存在しません。
# frozenset を作成する
languages = frozenset(["Python", "JavaScript", "Java"])
# 変更しようとするとエラーになる
# languages.add("C++") # AttributeError: 'frozenset' object has no attribute 'add'
# languages.remove("Java") # AttributeError: 'frozenset' object has no attribute 'remove'この不変性が frozenset の定義的な特徴です。frozenset を「変更」したい場合は、新しいものを作る必要があります。
# 元の frozenset
original = frozenset([1, 2, 3])
# 要素を追加した新しい frozenset を作成する
modified = frozenset(list(original) + [4])
print(original) # Output: frozenset({1, 2, 3})
print(modified) # Output: frozenset({1, 2, 3, 4})17.6.3) frozenset でも集合演算は使える
frozenset は通常のセットと同じ集合演算(和集合、積集合、差集合など)をすべてサポートします。
# frozenset で集合演算を行う
set_a = frozenset([1, 2, 3, 4])
set_b = frozenset([3, 4, 5, 6])
# Union
print(set_a | set_b) # Output: frozenset({1, 2, 3, 4, 5, 6})
# Intersection
print(set_a & set_b) # Output: frozenset({3, 4})
# Difference
print(set_a - set_b) # Output: frozenset({1, 2})
# Symmetric difference
print(set_a ^ set_b) # Output: frozenset({1, 2, 5, 6})演算では、通常のセットと frozenset を混在させることもできます。
regular_set = {1, 2, 3}
frozen_set = frozenset([3, 4, 5])
# 通常セットと frozenset の間の演算
result = regular_set | frozen_set
print(result) # Output: {1, 2, 3, 4, 5}
print(type(result)) # Output: <class 'set'> (result is a regular set)17.6.4) なぜ frozenset を使うのか?
frozenset を使う主な理由は、通常のセットではできない「辞書のキー」や「別のセットの要素」として使える点にあります。
# WRONG - 通常のセットは辞書のキーにできません
# regular_set = {1, 2, 3}
# my_dict = {regular_set: "value"} # TypeError: unhashable type: 'set'
# CORRECT - frozenset は辞書のキーにできます
frozen_set = frozenset([1, 2, 3])
my_dict = {frozen_set: "value"}
print(my_dict) # Output: {frozenset({1, 2, 3}): 'value'}
print(my_dict[frozen_set]) # Output: valuefrozenset を辞書キーとして使う実用例です。
# 座標ペアに関する情報を保存する
# 各座標は (x, y) の値からなる frozenset
location_data = {
frozenset([0, 0]): "origin",
frozenset([1, 0]): "east",
frozenset([1, 1]): "northeast"
}
# 場所を検索する
point = frozenset([1, 0])
print(location_data[point]) # Output: eastfrozenset は別のセットの要素にもできます。
# WRONG - 通常のセットはセットの要素にできません
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# CORRECT - frozenset はセットの要素にできます
set_of_frozensets = {
frozenset([1, 2]),
frozenset([3, 4]),
frozenset([5, 6])
}
print(set_of_frozensets)
# Output: {frozenset({1, 2}), frozenset({3, 4}), frozenset({5, 6})}グループを表現する実用例です。
# 各チームがプレイヤー ID の frozenset で表されるチームを表現する
tournament_teams = {
frozenset([101, 102, 103]), # Team A
frozenset([201, 202, 203]), # Team B
frozenset([301, 302, 303]) # Team C
}
# 特定のチームが登録されているか確認する
team_to_check = frozenset([101, 102, 103])
if team_to_check in tournament_teams:
print("Team is registered")
else:
print("Team not found")
# Output: Team is registered17.6.5) セットと frozenset の相互変換
通常のセットと frozenset は簡単に相互変換できます。
# 通常のセットを frozenset に変換する
regular = {1, 2, 3, 4}
frozen = frozenset(regular)
print(frozen) # Output: frozenset({1, 2, 3, 4})
# frozenset を通常のセットに変換する
frozen = frozenset([5, 6, 7, 8])
regular = set(frozen)
print(regular) # Output: {5, 6, 7, 8}
# これで通常のセットを変更できる
regular.add(9)
print(regular) # Output: {5, 6, 7, 8, 9}17.7) hashable と unhashable の型: 辞書キーやセット要素になれるもの(とハッシュの簡単な補足)
この章を通して、セットには入れられる型と入れられない型があることを見てきました。たとえば、整数や文字列のセットは作れますが、リストのセットは作れません。この制約があるのは、セットの要素(そして第 16 章で学んだ辞書のキー)は ハッシュ可能(hashable) である必要があるためです。
17.7.1) 「ハッシュ可能」とはどういう意味?
ハッシュ可能(hashable) なオブジェクトとは、生存期間中に決して変化しないハッシュ値を持つオブジェクトのことです。Python は hash() という組み込み関数でこのハッシュ値を計算します。
# hashable な型にはハッシュ値がある
print(hash(42)) # Output: 42
print(hash("Python")) # Output: (some large integer)
print(hash((1, 2, 3))) # Output: (some large integer)ハッシュ値は整数で、Python が内部的にセットや辞書の中でオブジェクトを素早く見つけるために使います。効率よく物を見つけるためのアドレスやインデックスのようなものだと考えてください。
重要な性質: オブジェクトが hashable であるためには、そのハッシュ値が生存期間中に一定でなければなりません。つまりオブジェクト自体が不変である必要があります。もしオブジェクトが変化できるなら、ハッシュ値も変わる必要があり、そうなるとセットや辞書が壊れてしまいます。
17.7.2) 不変な型は hashable である
Python の不変な組み込み型はすべて hashable で、セット要素や辞書キーに使えます。
# 整数は hashable
numbers = {1, 2, 3, 4, 5}
print(numbers) # Output: {1, 2, 3, 4, 5}
# 文字列は hashable
words = {"apple", "banana", "cherry"}
print(words) # Output: {'apple', 'banana', 'cherry'}
# タプルは hashable(すべての要素が hashable の場合)
coordinates = {(0, 0), (1, 1), (2, 2)}
print(coordinates) # Output: {(0, 0), (1, 1), (2, 2)}
# frozenset は hashable
frozen_sets = {frozenset([1, 2]), frozenset([3, 4])}
print(frozen_sets) # Output: {frozenset({1, 2}), frozenset({3, 4})}
# bool と None は hashable
mixed = {True, False, None, 42, "text"}
print(mixed) # Output: {False, True, None, 42, 'text'}17.7.3) 可変な型は hashable ではない
リスト、通常のセット、辞書のような可変型は、内容が変化し得るため hashable ではありません。
# リストは hashable ではありません
# my_set = {[1, 2, 3]} # TypeError: unhashable type: 'list'
# 通常のセットは hashable ではありません
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# 辞書は hashable ではありません
# my_set = {{"key": "value"}} # TypeError: unhashable type: 'dict'なぜ可変性が問題なのでしょうか?もしリストをセットに追加できたら何が起こるかを考えてみてください。
# 仮想的なシナリオ(実際には動きません)
# my_list = [1, 2, 3]
# my_set = {my_list} # これが動いたと仮定する
#
# # Python は [1, 2, 3] に基づいてハッシュを計算する
# # ここでリストを変更する:
# my_list.append(4) # これで [1, 2, 3, 4] になる
#
# # ハッシュ値が不正になります!セットが壊れてしまいます。このため Python は、可変オブジェクトがセットに入ったり辞書のキーとして使われたりするのを防いでいます。そうしないと内部データ構造が壊れてしまいます。
初心者によくある混乱: セット自体は可変(要素の追加・削除ができる)ですが、要素は不変である必要があります。初心者はこの概念上の区別を理解しないまま、セットに追加した後にオブジェクトを変更しようとすることがあります。
# よくある混乱: セットは可変だが、要素は不変である必要がある
# セットは可変 - 中身を変えられる
fruits = {'apple', 'banana'}
fruits.add('orange') # ✓ Works
fruits.remove('apple') # ✓ Works
# しかし要素は不変である必要がある - 変更できない
my_list = [1, 2, 3]
# my_set = {my_list} # ✗ TypeError: unhashable type: 'list'
# Why? my_list を追加後に変更できると、セットの内部
# 構造が壊れてしまうためです。
# タプルは不変なのでこれは動く
my_tuple = (1, 2, 3)
my_set = {my_tuple} # ✓ Works - tuples can't be modified17.7.4) タプルの特殊ケース
タプルは、その要素がすべて hashable の場合に限って hashable です。可変オブジェクトを含むタプルは hashable ではありません。
# 不変要素のみのタプル - hashable
good_tuple = (1, 2, "three")
my_set = {good_tuple} # Works: good_tuple is hashable
print(my_set) # Output: {(1, 2, 'three')}
# リストを含むタプル - hashable ではない
bad_tuple = (1, 2, [3, 4])
# my_set = {bad_tuple} # TypeError: unhashable type: 'list'これは理にかなっています。タプル自体は不変(どのオブジェクトを含むかは変えられない)でも、その中のオブジェクトが可変なら、タプル全体の「値」は変わり得るためです。
# 可変要素を含むタプルがハッシュ化できない理由を示す
inner_list = [1, 2]
my_tuple = (inner_list, 3)
# タプルの構造は固定だが、中のリストは変化できる
inner_list.append(3) # これで inner_list は [1, 2, 3] になる
# タプルは同じオブジェクトだが、「含まれる」データは変わってしまう17.7.5) hashable かどうかをテストする
オブジェクトが hashable かどうかは、ハッシュを計算できるかを試すことでテストできます。
# hashable かどうかをテストする
def is_hashable(obj):
"""オブジェクトが hashable かどうかを確認する。"""
try:
hash(obj)
return True
except TypeError:
return False
# さまざまな型をテストする
print(is_hashable(42)) # Output: True
print(is_hashable("text")) # Output: True
print(is_hashable((1, 2, 3))) # Output: True
print(is_hashable([1, 2, 3])) # Output: False
print(is_hashable({1, 2, 3})) # Output: False
print(is_hashable({"key": "value"})) # Output: False17.7.6) hashable な型のまとめ
Hashable(セット要素または dict キーにできる):
- 整数:
42 - 浮動小数点数:
3.14 - 文字列:
"text" - タプル(全要素が hashable の場合):
(1, 2, "three") - frozenset:
frozenset([1, 2, 3]) - 真偽値:
True,False - None:
None
Hashable ではない(セット要素または dict キーにできない):
- リスト:
[1, 2, 3] - 通常のセット:
{1, 2, 3} - 辞書:
{"key": "value"} - hashable ではない要素を含むタプル:
(1, [2, 3])
hashable を理解すると、適切なデータ構造を選びやすくなり、セットや辞書を扱う際によくあるエラーも避けられます。原則はシンプルです。オブジェクトが変化し得るならハッシュ化できず、ハッシュ化できないならセットに入れたり辞書のキーにしたりできません。