web-dev-qa-db-ja.com

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

キャッシュのために、dictにあるGET引数からキャッシュキーを生成する必要があります。

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

130
ThiefMaster

辞書がネストされていない場合、dictの項目でfrozensetを作成し、 hash() を使用できます。

hash(frozenset(my_dict.items()))

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

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

94
Imran

sorted(d.items())を使用するだけでは、安定したレポートを取得できません。 dの値の一部は辞書でもあり得、それらのキーは依然として任意の順序で出力されます。すべてのキーが文字列である限り、私は使用することを好みます:

json.dumps(d, sort_keys=True)

そうは言っても、ハッシュを異なるマシンまたはPythonバージョン間で安定させる必要がある場合、これが防弾であるかどうかはわかりません。 separatorsおよびensure_ascii引数を追加して、デフォルトの変更から自分自身を保護することができます。コメントをお願いします。

112
Jack O'Connor

EDITすべてのキーが文字列である場合、この回答を読み続ける前に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で動作すると想定しています。例が機能する限り、Ido

func.__code__ 

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

func.func_code
57
jomido

より明確なソリューションを次に示します。

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))  
11
smartnut007

以下のコードは、Python hash()関数の使用を回避します。これは、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=
7
Claudio Fahey

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
5
Steve Yeago

キーの順序を保持するには、hash(str(dictionary))またはhash(json.dumps(dictionary))の代わりに、迅速で不潔なソリューションをお勧めします。

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

JSONシリアル化できないDateTimeなどのタイプでも機能します。

4
shirk3y

サードパーティの 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
3
Eric

これを行うには maps ライブラリを使用できます。具体的には、 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ライブラリの著者です。

0
Pedro Cattori

一般的なアプローチは問題ありませんが、ハッシュ方式を検討することをお勧めします。

SHAは暗号強度のために設計されました(速度もですが、強度がより重要です)。これを考慮に入れることができます。したがって、セキュリティが何らかの形でここで重要でない限り、組み込みのhash関数を使用することはおそらく良い考えです。

0
Eli Bendersky