セット — 重複のない集まり
重複の取り除き方、2つの集まりを比べる4つの記号、{}が空のセットでない理由、そして順序がないとは実際にどういうことか。
どうやって学びますか?
解決する問題
1日の売上がリストで届きます — 1件の販売が1エントリです:
sold = ["pen", "bag", "pen", "ink", "pen"]質問は、何種類の商品が売れたかです。len(sold)は5と言いますが、それは販売の回数で、実際にあったのは3つの異なる商品でした。
前の章までの方法でもできます — 空のリストを始め、各項目について追加する前にif item not in unique:と確認します。6行あれば、動きます。
でも質問が「月曜と火曜の両方で売れた商品はどれ?」なら、入れ子のループが2つ必要です。「月曜だけなら?」 — もう1つ。こんな質問はあまりにもよくあるので、数学にも専門の分野があります。そしてPythonでは、その数学をそのまま書くことができます。
そのための受け皿がセットです:重複を一切持たず、「両方にある」「片方だけにある」という質問に、1つの記号だけで答えます。
この章を終える頃には
- セットを作り、リストから重複を取り除ける
|、&、-、^で2つのセットを比べられる{}が空のセットでない理由を説明できる- セットに順序がないこと、それが問題になるのはいつかを知っている
- 順序を保ちながら重複を取り除くパターンを書ける
前提条件: 辞書 — キーと値のペア.
セット — 重複のない集まり
sold = ["pen", "bag", "pen", "ink", "pen"]
unique = set(sold)
print(sorted(unique))
print(len(unique))['bag', 'ink', 'pen']
3set(...)はリストを受け取り、重複を取り除きます。3つのpenが1つになります。
表示の外側をsorted()で包んでいるのに注目してください — これは意図的なもので、理由はすぐ後で分かります。
中括弧で直接書くこともできます:
letters = {"a", "b", "a"}
print(sorted(letters))
print(len(letters))['a', 'b']
22つ目の"a"は黙って消えました。エラーはありません — セットには、同じものが2回現れるという考え方がありません。
{}は空のセットではない
not_a_set = {}
real_set = set()
print(type(not_a_set))
print(type(real_set))<class 'dict'>
<class 'set'>中括弧はもともと辞書のために使われたので、空の中括弧{}は辞書のままになっています。空のセットを書くには、set()と書く必要があります。
この間違いは最初は静かです:{}で始め、.add()を呼ぶとAttributeErrorが出ます。それを見ると、問題はaddであるかのように見えます。
順序はない
これがセットのいちばん重要な制限であり、しかもいちばん誤解されている性質です。
セットの中身には順序がありません。インデックスもスライスもありません:
tags = {"new", "sale"}
print(tags[0])TypeError: 'set' object is not subscriptableそして表示したときに見える順序も当てになりません — 同じプログラムを2回実行すると、違って見えることがあります。だからこの章のすべての例では、表示する前にソートしています:セットを直接表示して見えるものが、次に見えるとは限りません。
リストから重複を取り除くいちばん短い方法はlist(set(sold))ですが、元の順序も捨てられます。順序が大事なときは、セットとリストを一緒に使います:
sold = ["pen", "bag", "pen", "ink", "bag"]
seen = set()
unique = []
for item in sold:
if item not in seen:
seen.add(item)
unique.append(item)
print(unique)['pen', 'bag', 'ink']リストが順序を保ち、セットが見てきたものを覚えます。この組み合わせは、とてもよく使われるパターンです。
追加と削除
tags = {"new"}
tags.add("sale")
tags.add("new")
print(sorted(tags))
tags.discard("gone")
tags.remove("sale")
print(sorted(tags))['new', 'sale']
['new']addはものを1つ入れます — すでにあるときは何も起きず、誰も文句を言いません。リストのappendなら2つ目のコピーを作っていたでしょうが、セットではそうなりません。
削除には2つの形があり、この違いが役に立ちます:
tags = {"new"}
tags.remove("gone")KeyError: 'gone'removeは見つからないと止まります。discardは黙って何もしません。本来あるべきものなら、ないことがすぐ分かるようにremoveと書きます。
2つのセットを比べる
ここからが、セットが本当に役に立つ場面です:
monday = {"rafi", "ahmed", "bilal"}
tuesday = {"ahmed", "dia"}
print(sorted(monday | tuesday))
print(sorted(monday & tuesday))
print(sorted(monday - tuesday))
print(sorted(monday ^ tuesday))['ahmed', 'bilal', 'dia', 'rafi']
['ahmed']
['bilal', 'rafi']
['bilal', 'dia', 'rafi']4つの記号、4つの質問:
|— どちらかにある側(和集合)。2日のどちらかに来たすべての人。&— 両方にある側(積集合)。両日に来た人。-— 最初にあり、2つ目にはない側(差集合)。月曜だけに来た人。^— 片方にだけある側(対称差)。どちらか1日のみに来た人。
-は順序を気にする点に注目してください:monday - tuesdayとtuesday - mondayでは違う答えになります。他の3つは、どちらに読んでも同じ結果です。
この4つをループで書くと、どれも数行ずつになり、少なくとも1つは間違える可能性が高くなります。
何を入れられるのか
辞書のキーと同じルールです:入れるものは、変更できないものでなければなりません。
s = set()
s.add([1, 2])TypeError: unhashable type: 'list'理由が同じなので、メッセージも一字一句同じです — セットの内部もハッシュを使っているからです。文字列、数、タプルは問題ありませんが、リストと辞書は入れられません。
完全な例
sales.py:
# Which products sold on both days, and which only on one
monday = ["pen", "bag", "pen", "ink"]
tuesday = ["ink", "bottle", "pen", "ink"]
mon = set(monday)
tue = set(tuesday)
print("Monday sold :", sorted(mon), f"({len(monday)} sales, {len(mon)} products)")
print("Tuesday sold :", sorted(tue), f"({len(tuesday)} sales, {len(tue)} products)")
print()
print("Either day :", sorted(mon | tue))
print("Both days :", sorted(mon & tue))
print("Monday only :", sorted(mon - tue))
print("Exactly one day:", sorted(mon ^ tue))
print()
print("Was ink sold on Monday?", "ink" in mon)Monday sold : ['bag', 'ink', 'pen'] (4 sales, 3 products)
Tuesday sold : ['bottle', 'ink', 'pen'] (4 sales, 3 products)
Either day : ['bag', 'bottle', 'ink', 'pen']
Both days : ['ink', 'pen']
Monday only : ['bag']
Exactly one day: ['bag', 'bottle']
Was ink sold on Monday? True気づく価値のあることが3つあります。
元のリストはそのまま残しています。len(monday)は4回の販売、len(mon)は3つの商品と言います。セットに変換した瞬間、販売の回数は失われます — つまりセットはデータの代替ではなく、データに対して特定の種類の質問をするための道具です。
表示するたびにsorted()。それがないと、順序は偶然の並びに見え、実行のたびに変わる可能性があります。
書かれているのは"ink" in monであって、"ink" in mondayではありません。どちらもTrueですが、セットの検索はリストの検索より速いです — リストではPythonが1つずつ項目を比べ、セットではハッシュで目的の場所にまっすぐ行きます。4つしかなければ違いは分かりませんが、4万個になれば別です。
うまくいかないとき
AttributeError: 'dict' object has no attribute 'add' セットのつもりでs = {}と書いたものです。あれは空の辞書です。s = set()と書きましょう。
TypeError: 'set' object is not subscriptable s[0]と書いたものです。セットには順序がないので、インデックスもありません。特定の要素が必要なら、たぶん欲しかったのはリストか辞書です。そうでなければsorted(s)がリストをくれます。
KeyError: 'gone' セットにないものを消すよう、removeに頼んだものです。どちらかの状態が正常ならdiscardを使いましょう。
TypeError: unhashable type: 'list' リストをセットに入れようとしたものです。tuple(...)で変換しましょう。
list(set(...))の後に順序がバラバラになった それが正常な結果です。セットには順序がないからです。順序が必要なら、上のseenパターンかsorted()を使います。
==で2つのセットを比べたら、順序が違ってもTrueだった それで正解です。2つのセットは、同じものを含んでいれば等しい — 順序は関係ありません。リストでは[1, 2] == [2, 1]はFalseですが、セットでは{1, 2} == {2, 1}はTrueです。
ステップ 4 / 6 — 予想する
理解度をチェック
2つの空のコンテナを作っています。type()は何と報告するでしょう?
not_a_set = {}
real_set = set()
print(type(not_a_set))
print(type(real_set))- A<class 'dict'> <class 'set'>
- B<class 'set'> <class 'set'>
- C<class 'set'> <class 'dict'>
- D<class 'dict'> <class 'dict'>
&と-は、それぞれどの2つの質問に答えるでしょう?
monday = {"rafi", "ahmed", "bilal"}
tuesday = {"ahmed", "dia"}
print(sorted(monday & tuesday))
print(sorted(monday - tuesday))- A['bilal', 'rafi'] ['ahmed']
- B['ahmed', 'bilal', 'dia', 'rafi'] ['ahmed']
- C['ahmed'] ['bilal', 'rafi']
- D['ahmed'] ['dia']
セットの最初の要素を要求しています。何が起きるでしょう?
tags = {"new", "sale"}
print(tags[0])- Anew
- Bsale
- C`TypeError` — セットには順序がなく、インデックスもありません
- D`KeyError`
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
visitors.pyというファイルを書き、1日ずつの来訪者のリストを3つ用意しましょう。一部の名前が複数の日に現れるようにします。
そして次を表示します:
- 各日の来訪の数と、重複を除いた人数
- 3日間全体で重複を除いた人数
- 3日すべてに来た人
- 初日だけに来た人
- 最初の2日のどちらか1日だけに来た人
続いて2つの実験です:
- 2つの方法で重複を取り除きます —
list(set(...))と上のseenパターン — そして両方の結果を並べて表示します。違いは何でしょう? {}で空のセットを作ろうとして、それに.add()を呼んでみましょう。どんなエラーが返り、メッセージはどの種類のオブジェクトの名前を挙げるでしょう?
2つ目は小さい実験ですが、一度自分の目で見れば、この間違いで10分を失うことは二度となくなります。