Pythonでリストをdictキーとして使用できないのはなぜですか?


100

Python dictのキーとして使用できるものと使用できないものについて少し混乱しています。

dicked = {}
dicked[None] = 'foo'     # None ok
dicked[(1,3)] = 'baz'    # tuple ok
import sys
dicked[sys] = 'bar'      # wow, even a module is ok !
dicked[(1,[3])] = 'qux'  # oops, not allowed

つまり、タプルは不変の型ですが、その内部のリストを非表示にすると、キーにすることはできません。モジュール内のリストを簡単に非表示にできませんか?

キーは「ハッシュ可能」でなければならないという漠然とした考えがありましたが、技術的な詳細について自分の無知を認めるつもりです。ここで何が起こっているのかわかりません。リストをキーとして、たとえばハッシュをそれらのメモリ位置として使用しようとすると、何が問題になりますか?


1
ここでは良い議論です: stackoverflow.com/questions/2671211/...
エルナン・

49
変数名からくすくす笑いました。
キンドル

回答:


33

Pythonのウィキのトピックに良い記事があります:リストは辞書のキーできない理由。そこで説明したように:

リストをキーとして、たとえばハッシュをそれらのメモリ位置として使用しようとすると、何が問題になりますか?

要件を実際に破ることなく実行できますが、予期しない動作が発生します。リストは一般に、たとえば(不等)をチェックする場合など、コンテンツの値から派生したものとして扱われます。多くの人は、当然のことながら、どのリスト[1, 2]を使用しても同じキーを取得できることを期待しています。その場合、まったく同じリストオブジェクトを保持する必要があります。しかし、値としてのルックアップは、キーとして使用されるリストが変更されるとすぐに壊れます。IDによるルックアップでは、まったく同じリストを維持する必要があります。これは、他の一般的なリスト操作では必要ありません(少なくとも、私は考えられないものです) )。

モジュールなどの他のオブジェクトobjectは、とにかくオブジェクトIDをはるかに大きく処理し(最後にsys?と呼ばれる2つの異なるモジュールオブジェクトがあったとき)、それによってとにかく比較されます。したがって、dictキーとして使用した場合に、その場合もIDで比較することは、それほど驚くべきことではありません。


30

Pythonでリストをdictキーとして使用できないのはなぜですか?

>>> d = {repr([1,2,3]): 'value'}
{'[1, 2, 3]': 'value'}

(この質問に出くわした人がそれを回避する方法を探している場合)

ここで他の人が説明したように、実際にはできません。ただし、本当にリストを使用したい場合は、代わりにその文字列表現を使用できます。


5
申し訳ありませんが、あなたの意見は本当にわかりません。キーとして文字列リテラルを使用することと同じです。
WIM

11
真; 「キーはハッシュ可能でなければならない」という観点からリストを使用できない理由を実際に説明する非常に多くの回答を見ただけで、誰か(新しい)がそれを探している場合に備えて、それを回避する方法を提案したかった...
レミ

5
リストをタプルに変換しないのはなぜですか?なぜそれを文字列に変換するのですか?タプルを使用すると、カスタム比較メソッドを持つクラスで正しく機能します__eq__。しかし、それらを文字列に変換すると、すべてがその文字列表現によって比較されます。
Aran-Fey

良い点@ Aran-Fey。タプル内の要素自体がハッシュ可能であることを確認してください。たとえば、タプルの要素がリストであるため、キーとしてのtuple([[1,2]、[2,3]])は機能しません。
レミ

17

リストをタプルに変更し、それをキーとして使用できることがわかりました。

d = {tuple([1,2,3]): 'value'}

15

問題は、タプルは不変であり、リストは不変であるということです。以下を検討してください

d = {}
li = [1,2,3]
d[li] = 5
li.append(4)

何をd[li]返すべきですか?同じリストですか?いかがd[[1,2,3]]ですか?同じ値ですが、別のリストですか?

結局、満足できる答えはありません。たとえば、機能する唯一のキーが元のキーである場合、そのキーへの参照がないと、その値に再びアクセスすることはできません。許可されている他のすべてのキーを使用して、元のキーを参照せずにキーを作成できます。

私の提案の両方が機能する場合、同じ値を返す非常に異なるキーがありますが、これは少し驚くべきことです。オリジナルのコンテンツのみが機能する場合、リストは変更されるため、キーはすぐに機能しなくなります。


はい、それは同じリストなので、d[li]5のままでいることを期待します。 d[[1,2,3]]別のリストオブジェクトをキーとして参照するため、KeyErrorになります。本当に問題はまだありません。ただし、キーをガベージコレクションの対象にすると、dict値の一部にアクセスできなくなる場合があります。しかし、それは実用上の問題は論理的な問題ではないのです...
WIM

@wim:d[list(li)]KeyErrorであることは問題の一部です。他のほとんどすべての使用例liは、同じ内容の新しいリストと区別できません。機能しますが、多くの人にとって直感に反しています。前回の時プラス、あなたは本当に dictのキーとしてリストを使用しなければなりませんか?とにかくアイデンティティによるときにしているハッシュのすべてを私は想像することができる唯一のユースケースがあり、その場合にはあなただけに頼るの代わりに、それを行う必要があります__hash__し、__eq__アイデンティティベースであることを。

@delnan 問題は、そのような複雑さのためにそれほど有用な口述ではないということですか?それともそれが実際に口述を壊すことができるいくつかの理由がありますか?
wim

1
@wim:後者。私の回答で述べたように、dictキーの要件を実際に壊すことはありませんが、解決するよりも多くの問題を引き起こす可能性があります。

1
@delnan -あなたは「旧」を言うためのもの
ジェイソン・

9

これが答えですhttp://wiki.python.org/moin/DictionaryKeys

リストをキーとして、たとえばハッシュをそれらのメモリ位置として使用しようとすると、何が問題になりますか?

同じ内容のリストを比較すると同等であると示されていても、同じ内容の異なるリストを検索すると結果は異なります。

辞書検索でリストリテラルを使用するのはどうですか?


3

あなたの日除けはここにあります:

リストが辞書キーにならない理由

Pythonの初心者は、なぜタプルとリストタイプの両方が言語に含まれているのに、タプルは辞書キーとして使用できるのに、リストは使用できないのかと疑問に思うことがよくあります。これは意図的な設計上の決定であり、Python辞書がどのように機能するかを最初に理解することで最もよく説明できます。

ソースと詳細:http : //wiki.python.org/moin/DictionaryKeys


3

リストは変更可能であるため、dictキー(およびsetメンバー)はハッシュ可能である必要があります。ハッシュ値はインスタンス属性に基づいて計算する必要があるため、変更可能なオブジェクトのハッシュはお勧めできません。

この回答では、いくつかの具体的な例を挙げます。うまくいけば、既存の回答に加えて付加価値を加えます。すべての洞察は、データ構造の要素にもset適用されます。

例1:可変オブジェクトのハッシュ。ハッシュ値はオブジェクトの可変特性に基づいています。

>>> class stupidlist(list):
...     def __hash__(self):
...         return len(self)
... 
>>> stupid = stupidlist([1, 2, 3])
>>> d = {stupid: 0}
>>> stupid.append(4)
>>> stupid
[1, 2, 3, 4]
>>> d
{[1, 2, 3, 4]: 0}
>>> stupid in d
False
>>> stupid in d.keys()
False
>>> stupid in list(d.keys())
True

を変更した後stupid、ハッシュが変更されたため、dictでそれを見つけることができなくなりました。dictのキーのリストに対する線形スキャンのみが見つけstupidます。

例2:...しかし、なぜ一定のハッシュ値ではないのですか?

>>> class stupidlist2(list):
...     def __hash__(self):
...         return id(self)
... 
>>> stupidA = stupidlist2([1, 2, 3])
>>> stupidB = stupidlist2([1, 2, 3])
>>> 
>>> stupidA == stupidB
True
>>> stupidA in {stupidB: 0}
False

dictまたはでそれらを見つけることができるように、等しいオブジェクトは同一にハッシュする必要があるため、これも良い考えではありませんset

例3:...わかりました。すべてのインスタンスで定数ハッシュはどうですか?!

>>> class stupidlist3(list):
...     def __hash__(self):
...         return 1
... 
>>> stupidC = stupidlist3([1, 2, 3])
>>> stupidD = stupidlist3([1, 2, 3])
>>> stupidE = stupidlist3([1, 2, 3, 4])
>>> 
>>> stupidC in {stupidD: 0}
True
>>> stupidC in {stupidE: 0}
False
>>> d = {stupidC: 0}
>>> stupidC.append(5)
>>> stupidC in d
True

物事は期待どおりに機能しているように見えますが、何が起こっているのかを考えてください。クラスのすべてのインスタンスが同じハッシュ値を生成すると、にキーとして、dictまたはに存在するインスタンスが2つを超えると、ハッシュの衝突が発生しますset

my_dict[key]or key in my_dict(またはitem in my_set)を使用して適切なインスタンスを見つけるには、stupidlist3(最悪の場合)辞書のキーにあるインスタンスと同じ数の等価性チェックを実行する必要があります。この時点で、辞書の目的(O(1)ルックアップ)は完全に無効になっています。これは、次のタイミングで示されます(IPythonで実行)。

例3のタイミング

>>> lists_list = [[i]  for i in range(1000)]
>>> stupidlists_set = {stupidlist3([i]) for i in range(1000)}
>>> tuples_set = {(i,) for i in range(1000)}
>>> l = [999]
>>> s = stupidlist3([999])
>>> t = (999,)
>>> 
>>> %timeit l in lists_list
25.5 µs ± 442 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit s in stupidlists_set
38.5 µs ± 61.2 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit t in tuples_set
77.6 ns ± 1.5 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

ご覧のとおり、のメンバーシップテストstupidlists_setは全体の線形スキャンよりもさらに低速lists_listですが、ハッシュの衝突をロードすることなく、予想される超高速ルックアップ時間(係数500)がセットにあります。


TL; DR:タプルは不変でハッシュ可能であるためtuple(yourlist)dictキーとして使用できます。


>>> x =(1,2,3321321321321、)>>> id(x)139936535758888 >>> z =(1,2,3321321321321,1、)>>> id(z)139936535760544 >>> id((1、 2,3321321321321、))139936535810768これら3つのタプル値は同じですが、IDが異なります。それで、キーxを持つディクショナリにはキーzの値がありませんか?
アシュワニ

@Ashwani試してみましたか?
timgeb

はい、期待どおりに機能しています。私の疑問は、同じ値を持つすべてのタプルのIDが異なることです。それでは、このハッシュはどのような基準で計算されますか?
アシュワニ

ハッシュを@Ashwani xz同じです。それについて不明な点がある場合は、新しい質問を開いてください。
timgeb

1
@Ashwani hash(x)およびhash(z)
timgeb

1

あなたの質問への簡単な答えは、クラスリストが辞書のキーとして使用されることを希望するオブジェクトに必要なメソッドハッシュを実装しないということです。ただし、ハッシュが(コンテナーのコンテンツに基づく)タプルクラスと同じ方法で実装されていない理由は、リストが可変であるため、リストを編集するにはハッシュを再計算する必要があるため、リストが現在、基になるハッシュテーブル内の間違ったバケットに配置されています。タプル(不変)を変更することはできないため、この問題には遭遇しないことに注意してください。

補足として、dictobjectsルックアップの実際の実装は、Knuth Vol。3、Sec。6.4。この本が手元にある場合は、一読に値するかもしれません。また、本当に興味がある場合は、dictobjectの実際の実装に関する開発者のコ​​メントをここで確認してください。正確にどのように機能するかについては、非常に詳細に説明しています。興味があるかもしれない辞書の実装に関するpython講義もあります。最初の数分間で、キーの定義とハッシュの内容について説明します。


-1

Python 2.7.2ドキュメントによると:

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

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

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

タプルは、その要素を追加、削除、または置換できないという意味では不変ですが、要素自体は変更可能である場合があります。リストのハッシュ値は、その要素のハッシュ値に依存するため、要素を変更すると変更されます。

リストのハッシュにIDを使用すると、すべてのリストの比較が異なることを意味しますが、これは驚くべきことであり、不便です。


1
それは質問に答えません、そうですか?hash = id最初の段落の終わりで不変条件を壊さないので、問題はなぜそれがそのように行われないのかです。

@delnan:明確にするために最後の段落を追加しました。
Nicola Musatti、2011

-1

辞書は、キーのマップ、ハッシュされた新しいキーに変換された値、および値のマッピングを格納するHashMapです。

(疑似コード)のようなもの:

{key : val}  
hash(key) = val

辞書のキーとして使用できるオプションはどれか疑問に思っている場合。その後

ある何でもハッシュ可能に(ハッシュに変換することができ、かつホールド静的な値、すなわち不変前述したようにハッシュされたキーを作成するように)適格であるが、リストやセットオブジェクトをすることができて、ハッシュ(キー)そうすべきでもニーズが外出先で異なりリストまたはセットと同期するためだけに変化する。

あなたが試すことができます :

hash(<your key here>)

うまく機能すれば、辞書のキーとして使用したり、ハッシュ可能なものに変換したりできます。


要するに :

  1. そのリストを tuple(<your list>)
  2. そのリストをに変換しますstr(<your list>)

-1

dictキーはハッシュ可能である必要があります。リストは変更可能であり、有効なハッシュを提供していません方式を。

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