辞書をハッシュ化しますか?


156

キャッシュの目的で、dictに存在するGET引数からキャッシュキーを生成する必要があります。

現在私はsha1(repr(sorted(my_dict.items())))hashlibを内部sha1()で使用する便利な方法です)を使用していますが、もっと良い方法があるかどうか知りたいです。


4
これはネストされたdictでは機能しない可能性があります。最短の解決策は、代わりにjson.dumps(my_dict、sort_keys = True)を使用することです。これは、dict値に再帰します。
Andrey Fedorov 2014

2
参考までに:dumps、stackoverflow.com / a / 12739361/1082367は、「pickleからの出力は、dictとsetの順序が非決定的であるのと同様の理由で、正規であることが保証されていません。ハッシュにpickleまたはpprintまたはreprを使用しないでください。 」
Matthew Cornell

項目ではなく辞書キーをソートします。キーをハッシュ関数に送信します。
nyuwec

2
可変データ構造(辞書など)のハッシュに関する興味深い裏話:python.org/dev/peps/pep-0351は、オブジェクトを任意にフリーズできるように提案されましたが、拒否されました。根拠については、python-devの次のスレッドを参照してください:mail.python.org/pipermail/python-dev/2006-February/060793.html
FluxLemur

データがjson形式であり、意味的に不変のハッシュが必要な場合は、github.com/schollii/sandals/blob/master/json_sem_hash.pyを確認してください。ネストされた構造(もちろん、json以降)で機能し、保存された順序(Pythonの存続期間中に進化した)のようなdictの内部に依存せず、2つのデータ構造が意味的に同じである場合、同じハッシュを提供します( like {'a': 1, 'b':2}は意味的には)と同じ{'b':2, 'a':1}です。YMMVは複雑すぎてまだ使用していませんが、フィードバックは歓迎します。
オリバー

回答:


110

辞書がネストされていない場合は、dictのアイテムでフローズンセットを作成し、次を使用できますhash()

hash(frozenset(my_dict.items()))

これは、JSON文字列やディクショナリの表現を生成するよりも、計算負荷がはるかに少なくなります。

更新:以下のコメントを参照してください。なぜこのアプローチでは安定した結果が得られないのでしょうか。


9
これは、入れ子になった辞書では機能しませんでした。以下の解決策は試していません(複雑すぎます)。OPのソリューションは完璧に機能します。インポートを保存するために、sha1をハッシュに置き換えました。
Spatel

9
@Ceaserタプルは順序付けを意味するが、dict項目は順序付けされていないため、これは機能しません。frozensetの方が優れています。
アンチモン2012

28
異なるマシン間で一貫性を保つ必要がある場合は、組み込みハッシュに注意してください。HerokuやGAEなどのクラウドプラットフォームでのPythonの実装は、異なるインスタンスでhash()に異なる値を返し、2つ以上の「マシン」(herokuの場合はdyno)間で共有する必要があるものには役に立たない
ベンロバーツ

6
hash()関数が安定した出力を生成しないのは興味深いかもしれません。これは、同じ入力が与えられた場合、同じPythonインタープリターの異なるインスタンスで異なる結果を返すことを意味します。私には、インタープリターが開始されるたびに、ある種のシード値が生成されるように見えます。
Hermann Schachner、2015年

7
期待された。シードは、何らかのメモリのランダム化を追加することを覚えている限り、セキュリティ上の理由から導入されています。あなたはハッシュが2つのpythonのプロセス間で同じであることを期待することはできませんので
Nikokrock

137

を使用しsorted(d.items())ても、安定した収益を得るためには不十分です。の値の一部dも辞書である可能性があり、それらのキーは任意の順序で出力されます。すべてのキーが文字列である限り、以下を使用することをお勧めします。

json.dumps(d, sort_keys=True)

そうは言っても、ハッシュが異なるマシンまたはPythonバージョン間で安定している必要がある場合、これが完全なものであるかどうかはわかりません。separatorsensure_ascii引数を追加して、デフォルトの変更から保護することができます。コメントいただければ幸いです。


6
これは単なる偏執狂ですが、JSONはほとんどの文字をリテラルエスケープなしで文字列に表示できるため、エンコーダーは文字をエスケープするか、そのまま渡すかについていくつかの選択を行うことができます。リスクは、エンコーダーの異なるバージョン(または将来のバージョン)がデフォルトで異なるエスケープ選択を行う可能性があり、プログラムが異なる環境で同じディクショナリーに対して異なるハッシュ値を計算することです。ensure_ascii引数は、この完全に仮想的な問題から保護します。
Jack O'Connor 14

4
さまざまなデータセットを使用してこのパフォーマンスをテストしましたmake_hashgist.github.com/charlax/b8731de51d2ea86c6eb9
charlax

3
@charlax ujsonはdictペアの順序を保証しないため、そのようにするのは安全ではありません
arthurprs

11
このソリューションは、すべてのキーが文字列である場合にのみ機能します。たとえば、json.dumps({'a':{(0、5):5、1:3}})は失敗します。
kadee 2016年

5
@LorenzoBelliは、あなたが追加することによって、それを克服することができますdefault=strするdumpsコマンド。うまく機能しているようです。
mlissner 2018年

63

編集すべてのキーが文字列の場合、この回答を読み続ける前に、Jack O'Connorの非常にシンプルな(そして高速な)ソリューションを参照してください(ネストされた辞書のハッシュにも機能します)。

回答は受理されましたが、質問のタイトルは「Python辞書のハッシュ化」であり、タイトルに関しては回答が不完全です。(質問の本文に関しては、答えは完全です。)

ネストされた辞書

辞書をハッシュする方法をStack Overflowで検索すると、この適切にタイトルが付けられた質問に出くわし、ネストされた複数の辞書をハッシュしようとすると、満足できなくなります。この場合、上記の答えは機能せず、ハッシュを取得するために何らかの再帰的なメカニズムを実装する必要があります。

そのようなメカニズムの1つを次に示します。

import copy

def make_hash(o):

  """
  Makes a hash from a dictionary, list, tuple or set to any level, that contains
  only other hashable types (including any lists, tuples, sets, and
  dictionaries).
  """

  if isinstance(o, (set, tuple, list)):

    return tuple([make_hash(e) for e in o])    

  elif not isinstance(o, dict):

    return hash(o)

  new_o = copy.deepcopy(o)
  for k, v in new_o.items():
    new_o[k] = make_hash(v)

  return hash(tuple(frozenset(sorted(new_o.items()))))

おまけ:オブジェクトとクラスのハッシュ

このhash()関数は、クラスまたはインスタンスをハッシュするときに最適です。ただし、オブジェクトに関しては、ハッシュに関して私が見つけた1つの問題があります。

class Foo(object): pass
foo = Foo()
print (hash(foo)) # 1209812346789
foo.a = 1
print (hash(foo)) # 1209812346789

fooを変更した後でも、ハッシュは同じです。これは、fooのIDが変更されていないため、ハッシュは同じだからです。fooの現在の定義に応じて異なる方法でハッシュしたい場合、解決策は、実際に変更されているものはすべてハッシュ化することです。この場合、__dict__属性は次のとおりです。

class Foo(object): pass
foo = Foo()
print (make_hash(foo.__dict__)) # 1209812346789
foo.a = 1
print (make_hash(foo.__dict__)) # -78956430974785

悲しいかな、あなたがクラス自体で同じことをやろうとしたとき:

print (make_hash(Foo.__dict__)) # TypeError: unhashable type: 'dict_proxy'

クラス__dict__プロパティは通常の辞書ではありません:

print (type(Foo.__dict__)) # type <'dict_proxy'>

これは、クラスを適切に処理する、以前と同様のメカニズムです。

import copy

DictProxyType = type(object.__dict__)

def make_hash(o):

  """
  Makes a hash from a dictionary, list, tuple or set to any level, that 
  contains only other hashable types (including any lists, tuples, sets, and
  dictionaries). In the case where other kinds of objects (like classes) need 
  to be hashed, pass in a collection of object attributes that are pertinent. 
  For example, a class can be hashed in this fashion:

    make_hash([cls.__dict__, cls.__name__])

  A function can be hashed like so:

    make_hash([fn.__dict__, fn.__code__])
  """

  if type(o) == DictProxyType:
    o2 = {}
    for k, v in o.items():
      if not k.startswith("__"):
        o2[k] = v
    o = o2  

  if isinstance(o, (set, tuple, list)):

    return tuple([make_hash(e) for e in o])    

  elif not isinstance(o, dict):

    return hash(o)

  new_o = copy.deepcopy(o)
  for k, v in new_o.items():
    new_o[k] = make_hash(v)

  return hash(tuple(frozenset(sorted(new_o.items()))))

これを使用して、必要な要素のハッシュタプルを返すことができます。

# -7666086133114527897
print (make_hash(func.__code__))

# (-7666086133114527897, 3527539)
print (make_hash([func.__code__, func.__dict__]))

# (-7666086133114527897, 3527539, -509551383349783210)
print (make_hash([func.__code__, func.__dict__, func.__name__]))

注:上記のコードはすべてPython 3.xを想定しています。以前のバージョンではテストしていませんでしたmake_hash()が、たとえば2.7.2で動作すると思います。例を機能させる限り、私はそれを知っています

func.__code__ 

に置き換える必要があります

func.func_code

isinstanceは2番目の引数にシーケンスをとるため、isinstance(o、(set、tuple、list))が機能します。
Xealot 2013

frozensetがクエリ文字列パラメータを一貫してハッシュできることを理解させてくれてありがとう:)
Xealot

1
辞書アイテムの順序が異なるがキー値が異なる場合、同じハッシュを作成するためにアイテムをソートする必要があります-> return hash(tuple(frozenset(sorted(new_o.items()))))
Bas Koopmans

いいね!hashリストとタプルの周りへの呼び出しも追加しました。それ以外の場合は、たまたま私の辞書の値である整数のリストを受け取り、ハッシュのリストを返しますが、これは私が望むものではありません。
osa 2015年

frozensetはUNORDEREDコレクションなので、入力をソートしても何も得られません。一方、リストとタプルはORDEREDコレクション(「シーケンス」)であるため、ハッシュ値は内部のアイテムの順序に影響されます。あなたはそれらを並べ替えるべきではありません!
RobM

14

これはより明確な解決策です。

def freeze(o):
  if isinstance(o,dict):
    return frozenset({ k:freeze(v) for k,v in o.items()}.items())

  if isinstance(o,list):
    return tuple([freeze(v) for v in o])

  return o


def make_hash(o):
    """
    makes a hash out of anything that contains only list,dict and hashable types including string and numeric types
    """
    return hash(freeze(o))  

に変更if isinstance(o,list):するとif isinstance(obj, (set, tuple, list)):、この関数はどのオブジェクトでも機能します。
Peter Schorn

10

以下のコードは、Pythonの再起動間で一貫したハッシュを提供しないため、Pythonのhash()関数の使用を避けています(Python 3.3のハッシュ関数はセッション間で異なる結果を返すを参照)。make_hashable()オブジェクトをネストされたタプルにmake_hash_sha256()変換しrepr()、さらにbase64でエンコードされたSHA256ハッシュに変換します。

import hashlib
import base64

def make_hash_sha256(o):
    hasher = hashlib.sha256()
    hasher.update(repr(make_hashable(o)).encode())
    return base64.b64encode(hasher.digest()).decode()

def make_hashable(o):
    if isinstance(o, (tuple, list)):
        return tuple((make_hashable(e) for e in o))

    if isinstance(o, dict):
        return tuple(sorted((k,make_hashable(v)) for k,v in o.items()))

    if isinstance(o, (set, frozenset)):
        return tuple(sorted(make_hashable(e) for e in o))

    return o

o = dict(x=1,b=2,c=[3,4,5],d={6,7})
print(make_hashable(o))
# (('b', 2), ('c', (3, 4, 5)), ('d', (6, 7)), ('x', 1))

print(make_hash_sha256(o))
# fyt/gK6D24H9Ugexw+g3lbqnKZ0JAcgtNW+rXIDeU2Y=

1
make_hash_sha256(((0,1),(2,3)))==make_hash_sha256({0:1,2:3})==make_hash_sha256({2:3,0:1})!=make_hash_sha256(((2,3),(0,1)))。これは私が探している解決策ではありませんが、良い中間体です。type(o).__name__各タプルの先頭に追加して、差別化を強制することを考えています。
Poik

リストも並べ替える場合:tuple(sorted((make_hashable(e) for e in o)))
Suraj

make_hash_sha256()-いいね!
jtlz2

1
@Suraj内容が異なる順序であるリストはまったく同じものではないため、ハッシュする前にリストをソートする必要はありません。アイテムの順序が問題ではない場合、問題は間違ったデータ構造を使用していることです。リストではなくセットを使用する必要があります。
scottclowe

@scottcloweそれは本当です。その点を追加していただきありがとうございます。リストが必要なシナリオは2つあります(特定の注文は必要ありません)。1.繰り返しアイテムのリスト。2. JSONを直接使用する必要がある場合。JSONは「セット」表現をサポートしていないため。
スラジュ

5

2013年の返信から更新...

上記の答えはどれも私には信頼できるものではありません。その理由は、items()の使用です。私の知る限り、これはマシン依存の順序で出てきます。

代わりにこれはどうですか?

import hashlib

def dict_hash(the_dict, *ignore):
    if ignore:  # Sometimes you don't care about some items
        interesting = the_dict.copy()
        for item in ignore:
            if item in interesting:
                interesting.pop(item)
        the_dict = interesting
    result = hashlib.sha1(
        '%s' % sorted(the_dict.items())
    ).hexdigest()
    return result

なぜdict.items予測どおりに並べられたリストが返されないことが重要だと思いますか?frozensetそれを処理します
グラーレン14

2
セットは、定義により、順序付けされていません。したがって、オブジェクトが追加される順序は重要ではありません。組み込み関数hashは、frozensetの内容がどのように出力されるかなどを気にしないことを理解する必要があります。いくつかのマシンとpythonバージョンでテストしてみてください。
グラーレン14

value = hash( '%s ::%s'%(value、type(value)))で追加のhash()コールを使用するのはなぜですか?
RuiDo 2016

4

キーの順序を維持するために、代わりに、hash(str(dictionary))またはhash(json.dumps(dictionary))私は迅速で汚いソリューションを好みます:

from pprint import pformat
h = hash(pformat(dictionary))

DateTimeJSONシリアライズ可能ではないなどのタイプでも機能します。


3
pformatまたはjsonが常に同じ順序を使用することを誰が保証しますか?
ThiefMaster 2015年

1
@ThiefMaster、「バージョン2.5で変更:ディクショナリは表示が計算される前にキーでソートされます。2.5以前では、ディクショナリは、ドキュメントに記載されていませんでしたが、表示に複数行が必要な場合にのみソートされていました。」(docs.python。 org / 2 / library / pprint.html
Arel

2
これは私には有効ではないようです。pprintモジュールとpformatは、作者によって表示用であり、シリアル化ではないと理解されています。このため、pformatが常に機能する結果を返すと想定しても、安心してはいけません。
David Sanders

3

サードパーティのfrozendictモジュールを使用して、dictをフリーズしてハッシュ可能にすることができます。

from frozendict import frozendict
my_dict = frozendict(my_dict)

ネストされたオブジェクトを処理するには、次のようにします。

import collections.abc

def make_hashable(x):
    if isinstance(x, collections.abc.Hashable):
        return x
    elif isinstance(x, collections.abc.Sequence):
        return tuple(make_hashable(xi) for xi in x)
    elif isinstance(x, collections.abc.Set):
        return frozenset(make_hashable(xi) for xi in x)
    elif isinstance(x, collections.abc.Mapping):
        return frozendict({k: make_hashable(v) for k, v in x.items()})
    else:
        raise TypeError("Don't know how to make {} objects hashable".format(type(x).__name__))

より多くのタイプをサポートする場合は、functools.singledispatch(Python 3.7)を使用します。

@functools.singledispatch
def make_hashable(x):
    raise TypeError("Don't know how to make {} objects hashable".format(type(x).__name__))

@make_hashable.register
def _(x: collections.abc.Hashable):
    return x

@make_hashable.register
def _(x: collections.abc.Sequence):
    return tuple(make_hashable(xi) for xi in x)

@make_hashable.register
def _(x: collections.abc.Set):
    return frozenset(make_hashable(xi) for xi in x)

@make_hashable.register
def _(x: collections.abc.Mapping):
    return frozendict({k: make_hashable(v) for k, v in x.items()})

# add your own types here

これがために、例えば、動作しないdictDataFrameオブジェクト。
James Hirschorn

@JamesHirschorn:大声で失敗するように更新
Eric

いいね!s elifで動作するように次の句を追加しました。 回答を編集して、受け入れるかどうかを確認します...DataFrameelif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist())
James Hirschorn

1
OK。私は「ハッシュ可能」以上のものを求めていたようです。これは、等しいオブジェクトが同じハッシュを持つことを保証するだけです。私は...など、実行間に同じ値が得られますバージョンに取り組んで、とPythonのバージョンとは独立しています
ジェームズHirschorn

1
hashランダム化は、Python 3.7でデフォルトで有効になっている意図的なセキュリティ機能です。
Eric

1

これを行うには、マップライブラリを使用できます。具体的には、maps.FrozenMap

import maps
fm = maps.FrozenMap(my_dict)
hash(fm)

をインストールするにはmaps、次のようにします。

pip install maps

ネストされたdictケースも処理します。

import maps
fm = maps.FrozenMap.recurse(my_dict)
hash(fm)

免責事項:私はmaps図書館の作者です。


ライブラリは辞書内のリストを並べ替えません。したがって、これは異なるハッシュを生成する可能性があります。リストをソートするオプションもあるはずです。frozensetが役立つはずですが、dictsのリストを含むネストされたdictでケースをどのように処理しますか?辞書はハッシュ化できないので。
スラジュ

1
@Suraj:ネストされた構造をを介し処理し.recurseます。maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurseをご覧ください。リストの順序付けは意味的に意味があります。順序に依存しないようにする場合は、を呼び出す前にリストをセットに変換できます.recurse。また、使用することができますlist_fnし、パラメータを.recurseとは異なるハッシュ可能なデータ構造を使用するtuple(.eg frozenset
ペドロCattori

0

この問題に取り組む1つの方法は、辞書の項目のタプルを作成することです。

hash(tuple(my_dict.items()))

-8

私はそれをこのようにします:

hash(str(my_dict))

1
誰かがこの方法の何が悪いのか説明できますか?
mhristache 2016年

7
@maximiディクショナリは順序が安定していないため、hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))一部のディクショナリでは機能する可能性がありますが、すべてで機能するとは限りません。
Vlad Frolov 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.