見出し画像

文系でも分かる!Pythonプログラミング - mutable / immutable / hashable / unhashable

← preview

next →

今回は前回の続きでやんす。

mutable / immutable

mutable? / immutable?

>> dict型オブジェクトのkeyやvalueにできるオブジェクト

dict型 の key(キー) 
str型オブジェクト、
あるいはint型にする事がほとんどかと思いますが、

*immutable(イミュータブル)*なオブジェクトであれば

ぶっちゃけなんでも key にできます。

# *immutable*であればkeyになる

dic = {range(10) : "range型で呼び出された値だよ〜ん"}

print(dic[range(10)])

# >>> range型で呼び出された値だよ〜ん

一方、value(値)の方は、
関数 や 別のリスト、別の辞書 など
変数に代入できるようなオブジェクトは何でも格納できます。

リストに様々な型のオブジェクトを格納できたように
何でもござれのゴンザレス状態だと言えます。

# valueがrange型オブジェクトでも問題なしという話

dic = {"key":range(10)}

for i in dic["key"]:
	print(i,end="")

# >>> 0123456789

mutable

>> mutable ( ミュータブル )
= 可変、変更可能。

「mute」+「able」で は な く

「mutate(ミューテイト)」と 
「able(エイブル)」が組み合わされた語。

「mutate」とは「変異する」という意味の動詞である。
「able」とは「〜できる, 〜可能」という意味なので

「変異できる」→「変更可能(可変)」
という意味になった。

「突然変異体」のことを
「mutant(ミュータント)」と呼ぶ事がある。

併せて覚えると意味を思い出しやすい。


mutable(変更可能)なオブジェクト
以下の通りです。

  • list型

  • dict型

  • set型 (※別記事)

  • bytearray(バイトアレイ)型 (※別記事)

  • ユーザー定義クラス (※別記事)

「はぁ〜???」と思う型も書いてありますが、
別記事で紹介しますので今回はスルーしましょう。


*immutable*

>> immutable ( イミュータブル )
= 不変、変更不可能。

接頭辞in-」「im-」「ir-」「il-」は

「〜を欠いた」「〜が無い」「〜でない」
「不〜」「無〜」「非〜」
のように

後に続く単語に否定的な意味を付加しています。

例えば、

  • in-correct ( イン-コレクト ) = 正確な

  • in-complete ( イン-コンプリート ) = 完全な

  • in-expensive ( イン-エクスペンシブ ) = 高価でない

  • in-valid ( イン-ヴァリッド ) = 効な

  • im-possible ( イン-ポッシブル ) = 可能な

  • im-perfect ( イン-パーフェクト ) = 完全な

  • im-mutable ( -ュータブル ) = 変の

  • ir-regular ( -ギュラー ) = 規則な

  • il-legal ( -ーガル ) = 合法の

などの単語があります。

mutable」に否定を表す
im-」という接頭辞が付いていると覚えれば、

「imutable」 のようにスペルを間違えなくて済みますね。


*immutable(変更不可能)*なオブジェクト
以下の通りです。

  • int型

  • float型

  • complex(コンプレックス)型 (複素数)

  • bool型

  • str型

  • tuple型

  • range型

  • bytes(バイツ)型

  • frozenset(フローズンセット)型 (※別記事)

  • flie object

つまりこれらが
dict型keyにできるわけですね。


※僕独自の書き方で「*immutable*」と書いています。

「*👈雪っぽい」→「凍結」→「固まってる」→「不変

というイメージで覚えてもらうためにわざと付けています。

一般的には使われていません。


mutable / *immutable* とは?

簡単に言えば、

mutable = 値を 変 更 で き る オブジェクト
*immutable* = 値を 変 更 で き な い オブジェクト

という意味なのですが、

言葉だけで説明されても腑に落ちないと思います。

こちらをご覧下さい。👇

*immutable* (変更不可能)

👆このように、

変数aに1を代入した時のオブジェクトIDと
変数aに2を代入した時のオブジェクトIDを比較すると、
同値ではないことが分かります。

aは別モノになってしまっている
ということです。


しかし一方で、

変数aにリストを代入した後
そのリストに新しい値を追加しても

変数aのオブジェクトIDは変わりません。

mutable (変更可能)

値を追加したり削除したりしても
オブジェクトIDが変わらない。

リストや辞書なんかは
オブジェクトIDが変わらないからこそ
格納する値を自由に変更できる
とも言えます。

それが
mutable(ミュータブル)なオブジェクト
というわけです。


タプルに対して
append( )やremove( )のようなメソッドは使えません。

値の追加、削除、変更ができないのは、
 タプル *immutable*なオブジェクト だからです。

いえ、本当はタプルの見た目を
変化させること自体はできるんですよ。

tup_1 = ("A","B","C")

tup_2 = ("D",)

print(tup_1 + tup_2)

# >>> ('A''B''C''D')

👆タプルの連結。
値が追加されているように見える。

# 2つのタプルを連結してオブジェクトIDが変化するか観察

tup_1 = ("A","B","C")
tup_2 = ("D",)

print(id(tup_1))        # >>> 4657469984
print(id(tup_1+tup_2))  # >>> 4657544472

しかしながら、

2つのタプルを連結させて
新しいタプルを作っているだけ
なので、

オブジェクトとしては別モノになってしまいます。

(リストも連結したら
別のオブジェクトになりますがね。
)


👇あるいはこのように

# listに変換して
# 値を追加し
# tupleに戻す

tup = ("A","B","C")

print(id(tup))     # >>> 4930951424

l_tup = list(tup)  # list化

l_tup.append("D")  # 値の追加

tup = tuple(l_tup) # tuple化して同じ変数に再代入

print(id(tup))     # >>> 4673729624

list関数
タプルを一度リストにしてから

値の追加や削除などをして

再びtuple関数で
タプルに戻せば良い
のかもしれませんが

その場合も、
オブジェクトIDは変化してしまいます。

値を自由に変更したい場合はタプルじゃなくて
リストを使った方が良いですね。


hash

hash.

>> hash ( ハッシュ )

= 細切れ肉料理。寄せ集め、ごた混ぜ。
混乱、めちゃくちゃ。言い直し、作り直し。


ハッシュドポテトは
ジャガイモを細切れにしたりめちゃくちゃに潰して
まとめて揚げた料理ですね。それのイメージ。


dict(辞書) のように

keyとvalueをペアで管理するデータの構造のことを

「ハッシュテーブル(hash_table)」と呼ぶ事があります。

ここでの「table(テーブル)」は、
表(ひょう)」という意味です。


hash関数

いきなりですが、
hash( ) という 関数 を使ってみましょう。

引数(括弧内に書いて関数に渡す値)は、
int / float / str / bool 型にしてみます。

謎の数値

hash( )が返してきたこの数字(数値)は何?
って事なんですが...

この数値を「ハッシュ値」と言います。


ハッシュ値

このハッシュ値は、
通信の暗号化データの改竄(かいざん)防止に使われるものです。

元のデータを改竄されたり悪用されたりしないように
グッチャグチャな値に書き換えるわけですねえ。

ハッシュ値には、

オブジェクトの内容がちょっとでも違うと、
ハッシュ値も全くの別モノになる

という特徴があります。

空白文字を入れただけで
全然違うハッシュ値になった。

また、

ハッシュ値 
には 

どれだけ長いデータでもほぼ一定の長さになる

という特徴もあります。👇

一定の長さを保っている。

f = range(1,100) と g = range(1,100) 

同値ではありますが、
オブジェクトIDが違うので
同一オブジェクトではありません。


しかし、同値だとハッシュ値は同じになるようです。


衝突 ( collision )

hash関数を使って様々な値のハッシュ値を見ていると、
こんな問題に突き当たってしまいました。


また、このスクリプトを実行してみると...👇

for i in range(200):
	print(f"{i} =>",hash(2**i-1)) # iの2乗 -1
int型にhash( )を使うと
計算結果と同じハッシュ値になるのだなあ...
2の61乗-1 のハッシュ値が「0」!?!?!?

2の0乗-1 => 0
2の1乗-1 => 1
2の2乗-1 => 3
2の3乗-1 => 7
...
2の58乗-1 => 288230376151711743
2の59乗-1 => 576460752303423487
2の60乗-1 => 1152921504606846975

2の61乗-1 => 0
2の62乗-1 => 1
2の63乗-1 => 3
2の64乗-1 => 7
...
2の119乗-1 => 288230376151711743
2の120乗-1=> 576460752303423487
2の121乗-1 => 1152921504606846975

2の122乗-1 => 0
2の123乗-1 => 1
2の124乗-1 => 3
2の125乗-1 => 7
...

なんと、

2⁶¹-1、2¹²²-1、2¹⁸³-1...のように

2⁶¹*ⁿ -1 になった時に

ハッシュ値が「0」に戻って
他の数のハッシュ値とカブってしまう
事が
分かったのです。


このようにハッシュ値がカブってしまう現象
衝突(collision)」と言い、

同じハッシュ値を持つデータ群
シノニム(synonym)」と呼びます。


そう簡単には衝突は起こらないものの、

ハッシュ関数を用いてハッシュ化した値は、

理論上衝突が起こり得ます。

それが起こり得てしまうということは、

悪意を持った第三者がこの衝突を利用して
データをすり替えてしまう

という事が考えられるわけです。

これは「衝突攻撃」と呼ばれます。


ハッシュ値がカブったシノニム
処理する方法は以下の通りです。

● 「オープンアドレス法」
 = 空いているハッシュ値にデータを振り分けて衝突を解消する方法。

● 「直接連鎖法」
= 同じハッシュ値を持つデータを線形リストとして保存する方法。

線形リストって何なのよって話ですけど
今C言語も勉強し始めたところだから説明しません。
簡単に説明できるようになったら記事書いときますわ。


より衝突しにくい暗号にするには、
ハッシュ値がもっと長くて
使われる数の種類が多くなければなりません。

そのため、
hash関数で得られるハッシュ値より
強力なハッシュ値が生成できるアルゴリズムが作られています。

「SHA-1」は 160bit のハッシュ値を生成する。
「SHA-2」はバージョンによって異なるが、
224~512bit のハッシュ値を生成する。

https://www.sophia-it.com/content/衝突攻撃

ハッシュ値 は10進数だけとは限りません。

16進数(0~9とa~f)のハッシュ値にする
アルゴリズムもあります。

使える数が増えるので
より解読されにくい暗号にする事が
できるわけですね。

( 内容が高度になるので参考までに👇 )


ハッシュ値については、
800ページくらいある分厚い本にも
全く載っていませんでした。

ネット上には説明記事がありますが、
俺の大っ嫌いな専門用語ばっかりの説明記事しかなくて
クッソ腹が立ってます。

今はこれ以上深くは説明できません。ごめんな。

暗号化できるからと言って、手放しでは喜べないんだな
ということだけ覚えておきましょう。


ところで、
なぜ突然 ハッシュ値 なんてものが
出てきたかというと、

dict型 の key(キー) は、

#hashable (ハッシュナブル)
でなければならない


という決まりがあるからです。


#hashable  / unhashable

>> #hashable ( ハッシュナブル )

= ハッシュ化できるオブジェクト。

*immutable* なオブジェクトが
これに該当する。


※ tuple や frozenset は *immutable* だが、
要素が全て *immutable* である必要がある。(別記事)

>> unhashable ( アンハッシュナブル )

= ハッシュ化できないオブジェクト。

mutableなオブジェクトが
これに該当する。


辞書型 の key(キー) 

mutable
である list型 を適用してみると、

こんなエラーが出ます。

list関数をkeyにしたらエラーになった。

リストは中身を入れ替えても
オブジェクトIDが変わらない
mutable(変更可能) なオブジェクトです

mutableなオブジェクトは
ハッシュ化できない
ので、
dict型のkey(キー)にはできません。

これを「unhashable(アンハッシュナブル)」と言います。

dict型のkeyは、
#hashable  である必要がある
のです。


辞典や電話帳のような
膨大なデータの中から、
ある特定のを引き出す事を考えてください。

例えば、25万語くらい収録されている
広辞苑の中から

「エンジニアリング」という
見出し語 (keyと同じ役割) を探すときに、

1文字目を25万回比較して「"エ"」と等しい単語を集めて
2文字目を n回 比較して「"ン"」と等しい単語を集めて
3文字目を m回 比較して「"ジ"」と等しい単語を集めて
4文字目を....
5文字目を....

こんな風に比較をしまくっていたら
いつまで経っても値を引き出すことができません。

実際それをやってる「逐次検索(ちくじ-けんさく)」
という検索方法がありまして、

精度とか確実性は高いのだけれど、
いかんせん結果が出るまで時間が掛かってしまいます。


これが長いデータにだった場合は
もっと比較しなければなりませんし

検索ワードを修正するためとか、
何らかの誤った操作のせいで処理を一時中断しちゃって

最初から比較やり直し
なんてことになったら大変そうじゃないですか?


そこで、データの長さによって
検索に時間が掛かってしまわないように
ハッシュ値を使うわけです。

dict型では
key(キー)
を指定することで
value(値)
を呼び出せるわけですが、

その[key]のハッシュ値と
同じハッシュ値であるkeyを
辞書内から見つければいいわけですよ。

最悪でも辞書を1周すれば
探していたハッシュ値のkeyを見つけ出せるので

それとペアになっているvalue(値)を
すぐに引き出す事ができるのです。


ハッシュ値を使えば
高速に検索をすることができる。

だからkeyに限って
「#hashable(ハッシュ化できる)」オブジェクト
である必要があるわけですね。


そして、

「#hashable(ハッシュ化できる)オブジェクト」が

「*immutable*(変更不可能)オブジェクト」に限られるのは、

「value(値)」を引き出すための

「key(キー)」が別のモノに変更できたら困るから

なわけです。


次の記事へ。

いいなと思ったら応援しよう!