変数名の付け方と同じくらい、データの型大事にして、set型を使おう

# はじめに

データの型を大切にするのって大事だと思うんです。

わかりやすいコードを書きましょう。関数名や変数名は、働きのわかる名前をつけましょう。
こんな話は、プログラミングの基本ですが、データの型についても同じことが言えます。

データの型には、int, float, strなどの基本的な型があります。これについては、多くの方が理解していると思います。Pythonにはこれ以外に、List型に代表される、コレクション型があります。

この記事では、list型の陰に隠れてそうなset型についての説明です。

もし、set型を知らない（知っていても、実際の開発、設計で使ったことがない）という方は、データの型の大切さを再認識するいい機会かもしれません。

この記事では、Pythonを例にして説明しますが、Python以外の言語でも基本的には同じだと思います。

# コレクション型とは

コレクション型は、複数の要素をまとめて扱うための型で、複数の要素を一つの変数に格納することができます。

- リスト（list）
- タプル（tuple）
- 辞書（dict）
- 集合（set）

他にもあるんですが、この4つが基本的なコレクション型だと思うので、まずこの4種類の型について理解しておくといいと思います。
簡単に特徴を書いておきます。

## リスト

リストは、複数の要素を順序付きで格納するための型です。
順序付きということは、要素を追加した順番に格納されるということです。
順序があるので、インデックスを指定して要素を取り出すことができます。

数学の用語だと、数列のようなものです。

## タプル

タプルは、リストと似ていますが、要素を変更できないという違いがあります。
リストは、要素を追加、削除、変更することができますが、タプルは要素を変更することができません。
順序があるので、インデックスを指定して要素を取り出すことができます。

## 辞書

辞書は、キーと値をペアで格納するための型です。
キーを指定して値を取り出すことができます。
キーは、重複することができませんが、値は重複することができます。
辞書は、辞書の大きさが非常に大きな場合でも指定したキーに対する値を高速に取り出すことができるという特徴があります。
名前の通り、辞書の様に、多くのキーの中から指定したキーに対する値を取り出すことができます。

## 集合

集合は、重複しない要素を格納するための型です。
集合は、要素の順序がないので、インデックスを指定して要素を取り出すことができません。
インデクスを指定して取り出せませんが、代わりに、集合の中に指定した要素が含まれているかどうかを高速に判定することができます。
数学の用語だと、集合のようなものです。
また、集合同士の和集合、積集合、差集合などの演算を行うことができます。
Pythonのsetはこの集合型です。

# set使った事がありません！

tupleは、returnで複数の値を返す場合や、いろいろなライブラリーの関数の引数として使われているので、なじみがあるかと思います。
辞書は、Listみたいなものですが、キーの値を整数以外にできる特徴があるので、Listだと記述が難しいような処理が簡単にかける場合があり、活用する場面はありそうです。
それに比べると、集合(set)は、使用する場面が限られています。

しかし、ここに上げたコレクション型は、プログラミングをする上でどれも同じくらい重要で、setを使う場面も、実はたくさんあります。

# set型を使う例

set型は、集合を表すための型です。Listとの違いは、重複する要素を持たないという点です。

例えばどんなものがあるでしょうか。ChatGPTに例をあげてもらいました。

- 買い物リスト
- 訪れた場所
- アレルギーのある食品
- 電話番号の国際国番号
- 見た映画やドラマのタイトル
- 使用中のWi-Fi接続ポイント名（SSID）
- 所持している本のタイトル
- フォローしているSNSアカウントのID
- 趣味で集めているコレクションの種類
- 日記に記録した出来事のキーワード

このような事項を保存する場合は、set型を使うのが適しています。

# listでもいいんじゃない?

それなりに開発経験があるのに、set型を使った事がない人がいると思います。それは、set型を使わなくても、listで代用できるからです。
しかし、代用であって、set型を使う事で本来得られるメリットを失っているのです。

# set型のメリット

### 重複した値が存在しない事が保証される

重複したデータを渡してはいけない関数の引数をset型にしておけば、誤った使い方を防止する事ができます。
これがset型の最大のメリットです。

### 集合演算が使える

set型は、集合演算を行うことができます。和集合、積集合、差集合などの演算を行うことができます。

例えば、アレルギー物質を含んだ食材の表と、今日の食材の表があるとします。今日の食材にアレルギー物質を含んだ食材があるかどうかを調べる場合、set型を使うと、差集合を使って簡単に調べることができます。

```python
allergens = {"卵", "乳", "小麦", "そば", "落花生", "えび"}
todays_ingredients = {"卵", "牛乳", "小麦", "鶏肉", "じゃがいも"}

if allergens & todays_ingredients:
    print("アレルギー物質を含んだ食材が含まれています")
else:
    print("アレルギー物質を含んだ食材が含まれていません")

```

listの場合でも、自分で判定する処理を作れば同じことができますが、%だけでできるんですよ。

### データの特性を表現できる

プログラム他の人が見た時に、データの特性を表現できるのは、とても大事です。set型を使うことで、重複しない要素を持つことが、説明せずともわかります。
関数名や、変数名をその関数や変数の働きに合わせるのは、プログラムを読む人にとって、とてもわかりやすいです。
同じようにデータの型も、そのデータの特性を表現することができると、プログラムを読む人にとって、わかりやすくなります。

### 値が存在するかの判定が高速

set型は、値が存在するかどうかの判定を高速に行う事ができます。
これは、listでは真似ができない、大きな特徴なんですが、この特徴を発揮するには、次のような条件があります。
- データの件数が多いこと
- 同じデータに対して何度も検索すること
このような状況でないと、メリットを享受できないので、いままでsetを使わなくても困ってない分野の開発では、恩恵にあずかれる場合は少ないと思います。

しかし、条件に合致する場合は、処理時間を1/1000にするレベルで、高速化できます。
どれくらい違うのか、実際のプログラムで、紹介します。

```python
import random
import time

n = 1000000
# ランダムな値をn個作成
values = random.sample(range(n * 10), n)

# setとlistにする
set_values = set(values)
list_values = list(values)

# 値が存在するかチェックする処理
def check_values(values):
  start = time.time()
  cnt = 0
  for i in range(1000):
    x = random.randint(0, n * 10)
    if x in values:
      cnt += 1
  print(f'{cnt=}')
  end = time.time()
  return end - start

# setとlistで時間を比較
elapsed_time = check_values(set_values)
print(f'setの処理時間: {elapsed_time:0.4f} sec')
elapsed_time = check_values(list_values)
print(f'listの処理時間: {elapsed_time:0.4f} sec')
  
```

実行結果は、次のようになります。
```
cnt=116
setの処理時間: 0.0010 sec
cnt=97
listの処理時間: 9.7650 sec
```

set型のほうが圧倒的に速いですの圧勝ですね。1万倍速いです。

同じ処理でも、データの数を少なくして、n=100の場合だとこれほど大きな差にはなりません。
```
cnt=95
setの処理時間: 0.0008 sec
cnt=108
listの処理時間: 0.0016 sec
```

一つのsetに対して繰り返し、検索を行う場合に高速になります。
次のプログラムは、データはlistで保存していて、検索するときに、setに変換しています。
一つのsetに対して一回しか検索していません。

```
def check_values(values: list):
  start = time.time()
  cnt = 0
  for i in range(1000):
    x = random.randint(0, n * 10)
    if x in set(values): #←listからsetを作って検索している
      cnt += 1
  print(f'{cnt=}')
  end = time.time()
  print(f'set: {end - start:0.5f} sec')
```

これは、非常に遅くなります。実行してみると、終わりません。
理由は、`set(values)`でlistからsetを作るときに、データ全体を読み取っているので、list全体を検索する以上の処理時間がかかるからです。
setは、あらかじめ検索が高速になるように工夫をしてデータを保存しているから高速に検索ができるのです。

# setのデメリット

デメリットとしては、次のような点が挙げられます。

### 順序がない

set型は、要素の順序がないので、インデックスを指定して要素を取り出すことができません。
そのため、要素を取り出す際には、for文を使って要素をひとつづつ取り出す事はできますが、取り出す順番を指定したり、特定の順番のものを取り出すことはできません。
そもそも、順序がないので、順番を指定することができません。

### 要素の変更ができない

set型は、要素の変更ができません。
listのように何番目の要素というような概念がないので、値を変更するということができません。
値を削除して、新しい値を追加することはできます。

# まとめ

- set型は、重複しない要素を持つ集合を表すための型です。
- 順序のないデータを扱う場合に使います。
- listで代用してしまいがちですが、setが適切な部分にはsetを使いましょう。