文系でも分かる!Pythonプログラミング - mutable / immutable / hashable / unhashable
← preview
next →
今回は前回の続きでやんす。
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* = 値を 変 更 で き な い オブジェクト
という意味なのですが、
言葉だけで説明されても腑に落ちないと思います。
こちらをご覧下さい。👇
👆このように、
変数aに1を代入した時のオブジェクトIDと
変数aに2を代入した時のオブジェクトIDを比較すると、
同値ではないことが分かります。
aは別モノになってしまっている
ということです。
しかし一方で、
変数aにリストを代入した後
そのリストに新しい値を追加しても
変数aのオブジェクトIDは変わりません。
値を追加したり削除したりしても
オブジェクト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 ( ハッシュ )
= 細切れ肉料理。寄せ集め、ごた混ぜ。
混乱、めちゃくちゃ。言い直し、作り直し。
ハッシュドポテトは
ジャガイモを細切れにしたりめちゃくちゃに潰して
まとめて揚げた料理ですね。それのイメージ。
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
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関数で得られるハッシュ値より
強力なハッシュ値が生成できるアルゴリズムが作られています。
ハッシュ値 は10進数だけとは限りません。
16進数(0~9とa~f)のハッシュ値にする
アルゴリズムもあります。
使える数が増えるので
より解読されにくい暗号にする事が
できるわけですね。
( 内容が高度になるので参考までに👇 )
ハッシュ値については、
800ページくらいある分厚い本にも
全く載っていませんでした。
ネット上には説明記事がありますが、
俺の大っ嫌いな専門用語ばっかりの説明記事しかなくて
クッソ腹が立ってます。
今はこれ以上深くは説明できません。ごめんな。
暗号化できるからと言って、手放しでは喜べないんだな
ということだけ覚えておきましょう。
ところで、
なぜ突然 ハッシュ値 なんてものが
出てきたかというと、
dict型 の key(キー) は、
#hashable (ハッシュナブル)
でなければならない
という決まりがあるからです。
#hashable / unhashable
>> #hashable ( ハッシュナブル )
= ハッシュ化できるオブジェクト。
*immutable* なオブジェクトが
これに該当する。
※ tuple や frozenset は *immutable* だが、
要素が全て *immutable* である必要がある。(別記事)
>> unhashable ( アンハッシュナブル )
= ハッシュ化できないオブジェクト。
mutableなオブジェクトが
これに該当する。
辞書型 の key(キー) に
mutableである list型 を適用してみると、
こんなエラーが出ます。
リストは中身を入れ替えても
オブジェクト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(キー)」が別のモノに変更できたら困るから
なわけです。
次の記事へ。