ハッシュ可能、不変


81

最近のSOの質問(リストでインデックス付けされたPythonでの辞書の作成を参照)から、Pythonでのハッシュ可能オブジェクトと不変オブジェクトの意味について間違った概念を持っている可能性があることに気付きました。

  • 実際のハッシュ可能とはどういう意味ですか?
  • ハッシュ可能と不変の関係は何ですか?
  • ハッシュ可能である可変オブジェクトまたはハッシュ可能ではない不変オブジェクトはありますか?

回答:


85

ハッシュとは、大量のデータを繰り返し可能な方法で非常に少量(通常は単一の整数)に変換して、テーブルで一定時間(O(1))で検索できるようにするプロセスです。これは、高性能にとって重要です。アルゴリズムとデータ構造。

不変性とは、オブジェクトが作成された後、特にそのオブジェクトのハッシュ値を変更する可能性のある方法で、オブジェクトが重要な方法で変更されないという考え方です。

ハッシュキーとして使用されるオブジェクトは通常、ハッシュ値が変更されないように不変でなければならないため、2つのアイデアは関連しています。変更が許可された場合、ハッシュテーブルなどのデータ構造内のそのオブジェクトの場所が変更され、効率を高めるためのハッシュの目的全体が無効になります。

アイデアを実際に理解するには、C / C ++などの言語で独自のハッシュテーブルを実装するか、HashMapクラスのJava実装を読む必要があります。


1
さらに、ハッシュテーブルがキーのハッシュがいつ変更されたかを検出することはできません(少なくとも効率的な方法で)。これはよくある落とし穴です。たとえば、Javaで、HashMapキーとして使用されているオブジェクトを変更すると、が壊れてしまいます。マップを印刷しても、古いキーも新しいキーも見つかりません。
2015年

1
HashableとImmutableは多少関連していますが、同じではありません。たとえば、継承objectするカスタムクラスから作成されたインスタンスはハッシュ可能ですが、不変ではありません。これらのインスタンスはdictのキーを持って使用できますが、渡されれば変更できます。
Pranjal Mittal 2017

13
  • ハッシュ可能である可変オブジェクトまたはハッシュ可能ではない不変オブジェクトはありますか?

Pythonでは、タプルは不変ですが、すべての要素がハッシュ可能である場合にのみハッシュ可能です。

>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
TypeError: unhashable type: 'list'

ハッシュ可能なタイプ

  • アトミック不変型は、str、bytes、numeric型など、すべてハッシュ可能です。
  • 凍結されたセットは常にハッシュ可能です(その要素は定義上ハッシュ可能でなければなりません)
  • タプルは、そのすべての要素がハッシュ可能である場合にのみハッシュ可能です
  • ユーザー定義型は、ハッシュ値がid()であるため、デフォルトでハッシュ可能です。

8

Python用語集から:

オブジェクトは、その存続期間中に変更されないハッシュ値を持っている場合(__hash__()メソッドが必要)、ハッシュ可能であり、他のオブジェクトと比較できます(__eq__()または__cmp__()メソッド)です。等しいと比較するハッシュ可能なオブジェクトは、同じハッシュ値を持っている必要があります。

これらのデータ構造は内部でハッシュ値を使用するため、ハッシュ可能性により、オブジェクトは辞書キーおよびセットメンバーとして使用可能になります。

Pythonの不変の組み込みオブジェクトはすべてハッシュ可能ですが、変更可能なコンテナー(リストや辞書など)はハッシュ可能ではありません。ユーザー定義クラスのインスタンスであるオブジェクトは、デフォルトでハッシュ可能です。それらはすべて等しくなく比較され、それらのハッシュ値はそれらのid()です。

ディクトとセットは、ハッシュテーブルで効率的にルックアップするためにハッシュを使用する必要があります。ハッシュを変更するとデータ構造が台無しになり、dictまたはsetが失敗するため、ハッシュ値は不変である必要があります。ハッシュ値を不変にする最も簡単な方法は、オブジェクト全体を不変にすることです。そのため、この2つはよく一緒に言及されます。

組み込みの可変オブジェクトはいずれもハッシュ可能ではありませんが、可変ではないハッシュ値を使用して可変オブジェクトを作成することは可能です。オブジェクトの一部のみがそのアイデンティティを表すのが一般的ですが、オブジェクトの残りの部分には自由に変更できるプロパティが含まれています。ハッシュ値と比較関数がIDに基づいており、可変プロパティに基づいておらず、IDが変更されない限り、要件は満たされています。


@Andrey:凍結セットはハッシュ可能ですが、セットはハッシュ可能ではありません。どちらにもハッシュ可能なアイテムのみを含めることができます。マークがセットについて言及した場所では、彼は正しかったので、彼が冷凍セットを意味したとは思わない。
tzot 2010

12
ユーザー定義クラスは、デフォルトでハッシュ可能なタイプを定義します(ハッシュは単なるオブジェクトですid)。これはオブジェクトの存続期間中は変更できないため、ハッシュ可能ですが、可変タイプを定義できないという意味ではありません。申し訳ありませんが、ハッシュ可能性は不変性を意味するものではありません。
スコットグリフィス

1
@ScottGriffithsあなたのコメントを見るのに6年かかった理由はわかりませんが、今までにないほど遅くなりました。可変オブジェクトをC ++セットに入れることができないことを嘆いていたので、どうしてこんなに遠く離れていたのかわかりません。私の編集で問題が解決することを願っています。
マークランサム

7

技術的には、ハッシュ可能とは、クラスがを定義することを意味します__hash__()。ドキュメントによると:

__hash__()整数を返す必要があります。唯一必要なプロパティは、等しいと比較するオブジェクトが同じハッシュ値を持つことです。オブジェクトの比較においても役割を果たすオブジェクトのコンポーネントのハッシュ値を何らかの方法で(排他的論理和を使用するなどして)混合することをお勧めします。

Pythonの組み込み型の場合、ハッシュ可能な型もすべて不変だと思います。

それでも定義されている可変オブジェクトを持つことは困難ですが、おそらく不可能ではありません__hash__()


1
__hash__オブジェクトのid;を返すようにデフォルトで定義されていることは注目に値します。__hash__ = Noneハッシュ化できないように設定するには、邪魔にならないようにする必要があります。また、Mark Ransomが言及しているように、ハッシュ値が決して変更できない場合にのみハッシュ可能であるという追加の条件があります。
スコットグリフィス

5
私は、答えは誤解を招く少しだと思うlist定義__hash__という意味でhasattr([1,2,3], "__hash__")リターンがTrue、しかし、呼び出しhash([1,2,3])昇給をTypeError、それはまさにハッシュ可能ではありませんので、(Pythonの3)。の存在に依存__hash__するだけでは、何かがa)ハッシュ可能b)不変であるかどうかを判断するのに十分ではありません
Matti Lyra

4

相互作用のために不変とハッシュ可能の間に強制された明示的な関係がない場合でも、暗黙的です

  1. 等しいと比較するハッシュ可能なオブジェクトは、同じハッシュ値を持っている必要があります
  2. オブジェクトの存続期間中に変更されないハッシュ値がある場合、そのオブジェクトはハッシュ可能です。

__eq__オブジェクトクラスが値の同等性を定義するように再定義しない限り、ここでは問題はありません。

それが済んだら、同じ値を表すオブジェクトに対して常に同じ値を返す安定したハッシュ関数を見つける必要があります(たとえば、ここで__eq__)はTrueを返し、オブジェクトの存続期間中は変更されません。

これが可能なアプリケーションを見つけるのは難しいので、これらの要件を満たす可能性のあるクラスAを検討してください。__hash__定数を返す明らかな退化したケースがありますが。

今:-

>>> a = A(1)
>>> b = A(1)
>>> c = A(2)
>>> a == b
True
>>> a == c
False
>>> hash(a) == hash(b)
True
>>> a.set_value(c)
>>> a == c
True
>>> assert(hash(a) == hash(c)) # Because a == c => hash(a) == hash(c)
>>> assert(hash(a) == hash(b)) # Because hash(a) and hash(b) have compared equal 
                                 before and the result must stay static over the objects lifetime.

実際、これは、作成時にhash(b)== hash(c)を意味しますが、同等に比較されることはありません。とにかく__hash__、値による比較を定義する可変オブジェクトの()を便利に定義するのに苦労しています。

__lt____le____gt____ge__あなたはまだ自分の価値に基づいて可変あるいは、ハッシュ可能オブジェクトの順序を定義することができるようにcomparsionsは影響を受けません。


3

これがGoogleのトップヒットであるという理由だけで、可変オブジェクトをハッシュ可能にする簡単な方法は次のとおりです。

>>> class HashableList(list):
...  instancenumber = 0  # class variable
...  def __init__(self, initial = []):
...   super(HashableList, self).__init__(initial)
...   self.hashvalue = HashableList.instancenumber
...   HashableList.instancenumber += 1
...  def __hash__(self):
...   return self.hashvalue
... 
>>> l = [1,2,3]
>>> m = HashableList(l)
>>> n = HashableList([1,2,3])
>>> m == n
True
>>> a={m:1, n:2}
>>> a[l] = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> m.hashvalue, n.hashvalue
(0, 1)

SQLAlchemyレコードを変更可能で、より便利なものにキャストするクラスを作成するときに、dictキーとして使用するためのハッシュ機能を維持しながら、このような用途を実際に見つけました。


3

不変とは、オブジェクトがその存続期間中に重要な方法で変更されないことを意味します。これは、プログラミング言語では漠然としていますが一般的な考え方です。

ハッシュ性は若干異なり、比較を指します。

hashableオブジェクトは、その存続期間中に変更されないハッシュ値(__hash__()メソッドが必要)を持ち、他のオブジェクトと比較できる(__eq__()or__cmp__()メソッドが必要)場合にハッシュ可能です。等しいと比較するハッシュ可能なオブジェクトは、同じハッシュ値を持っている必要があります。

すべてのユーザー定義クラスには __hash__メソッドがあり、デフォルトではオブジェクトIDを返すだけです。したがって、ハッシュ可能性の基準を満たすオブジェクトは、必ずしも不変であるとは限りません。

宣言した新しいクラスのオブジェクトは、たとえばからスローすることによって防止しない限り、辞書キーとして使用できます。 __hash__

オブジェクトの存続期間中にハッシュが変更された場合、それはオブジェクトが変更されたことを意味するため、すべての不変オブジェクトはハッシュ可能であると言えます。

しかし、完全ではありません。リスト(可変)を持つタプルについて考えてみます。タプルは不変であると言う人もいますが、同時にハッシュ可能ではありません(スロー)。

d = dict()
d[ (0,0) ] = 1    #perfectly fine
d[ (0,[0]) ] = 1  #throws

ハッシュ可能性と不変性は、タイプではなくオブジェクトインスタンスを参照します。たとえば、タプル型のオブジェクトはハッシュ可能かどうかを指定できます。


1
「等しいと比較するハッシュ可能なオブジェクトは、同じハッシュ値を持っている必要があります。」どうして?同等に比較するが、同じハッシュ値を持たないオブジェクトを作成できます。
endolith 2014

1
このようなオブジェクトを作成することは可能ですが、Pythonドキュメントで定義されている概念に違反することになります。実際、この要件を使用して、そのような(論理的に同等の)含意を導き出すことができるという考え方です。ハッシュが等しくない場合、オブジェクトは等しくありません。非常に便利。多くの実装、コンテナ、およびアルゴリズムは、物事をスピードアップするためにこの含意に依存しています。
user2622016 2014

一般的な例comparison != identityのように一緒に「無効」の値を比較しているfloat("nan") == float("nan")、またはスライスからのインターンの文字列:"apple" is "apple""apple" is "crabapple"[4:]
sleblanc

1

Pythonでは、ほとんど互換性があります。ハッシュはコンテンツを表すことになっているため、オブジェクトと同じように変更可能であり、オブジェクトにハッシュ値を変更させると、dictキーとして使用できなくなります。

他の言語では、ハッシュ値はオブジェクトの「アイデンティティ」に関連しており、(必然的に)値に関連していません。したがって、可変オブジェクトの場合、ポインターを使用してハッシュを開始できます。もちろん、オブジェクトがメモリ内で移動しないと仮定します(一部のGCのように)。これは、たとえばLuaで使用されているアプローチです。これにより、可変オブジェクトがテーブルキーとして使用できるようになります。しかし、初心者にとってはいくつかの(不快な)驚きを生み出します。

結局、不変のシーケンスタイプ(タプル)を持つことで、「複数値キー」に適しています。


3
@javier: 'Pythonではほとんど互換性があります'私の疑問は 'ほとんど'に含まれていない小さな部分を指します
joaquin 2010

0

ハッシュ可能とは、変数の値を定数(文字列、数値など)で表す(またはエンコードする)ことができることを意味します。現在、変更される(変更可能な)ものは、そうでないもので表すことはできません。したがって、可変である変数はハッシュ可能にすることはできず、同じように、不変変数のみをハッシュ可能にすることができます。

お役に立てれば ...

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.