关于python:哈希字典?

Hashing a dictionary?

为了缓存的目的,我需要从dict中存在的get参数生成一个缓存键。

目前我正在使用sha1(repr(sorted(my_dict.items())))(sha1()是一种内部使用hashlib的方便方法),但我很好奇是否有更好的方法。


使用sorted(d.items())不足以让我们获得稳定的报告。d中的一些值也可能是字典,它们的键仍将以任意顺序出现。只要所有键都是字符串,我喜欢使用:

1
json.dumps(d, sort_keys=True)

也就是说,如果散列需要在不同的机器或Python版本中保持稳定,我不确定这是否是防弹的。您可能需要添加separatorsensure_ascii参数,以保护自己不受默认值的任何更改。我很感谢你的评论。


如果您的字典没有嵌套,您可以使用dict的项创建一个frozenset,并使用hash()

1
hash(frozenset(my_dict.items()))

这比生成JSON字符串或字典的表示要少得多的计算量。


编辑:如果您的所有键都是字符串,那么在继续阅读此答案之前,请参阅Jack O'Connor的明显更简单(更快)的解决方案(也适用于哈希嵌套字典)。

虽然已经接受了一个答案,但问题的标题是"hashing a python dictionary",关于这个标题,答案是不完整的。(关于问题主体,答案是完整的。)

嵌套词典

如果搜索堆栈溢出以查找如何散列字典,可能会偶然发现这个标题合适的问题,如果试图散列多个嵌套字典,可能会留下不满意的结果。在这种情况下,上面的答案不起作用,您必须实现某种递归机制来检索散列。

这是一个这样的机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
2
3
4
5
class Foo(object): pass
foo = Foo()
print (hash(foo)) # 1209812346789
foo.a = 1
print (hash(foo)) # 1209812346789

散列是一样的,即使在我改变了foo之后。这是因为foo的标识没有更改,所以散列是相同的。如果您希望foo根据其当前定义进行不同的散列,那么解决方案是散列实际更改的内容。在这种情况下,"听写"属性:

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

唉,当你试图对类本身做同样的事情时:

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

class_uu dict_uuuu属性不是普通字典:

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

这里有一个与前面类似的机制,可以适当地处理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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()))))

您可以使用它返回一个哈希元组,其中包含您想要的任意多个元素:

1
2
3
4
5
6
7
8
# -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中工作。至于让这些例子起作用,我知道

1
func.__code__

应替换为

1
func.func_code


这里有一个更清晰的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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))

下面的代码避免使用python hash()函数,因为它不会在python重新启动时提供一致的哈希(请参见python 3.3中的hash函数在会话之间返回不同的结果)。make_hashable()将对象转换为嵌套元组,make_hash_sha256()也将repr()转换为base64编码的sha256散列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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=


2013年更新回复…

以上的答案对我来说都不可靠。原因是使用了items()。据我所知,这是一个依赖于机器的顺序。

改成这个怎么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
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


为了保留关键订单,我希望快速而肮脏的解决方案,而不是hash(str(dictionary))hash(json.dumps(dictionary))

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

它甚至适用于DateTime等不可序列化的类型。


您可以使用第三方frozendict模块冻结听写并使其可哈希。

1
2
from frozendict import frozendict
my_dict = frozendict(my_dict)

对于处理嵌套对象,可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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


您可以使用地图库来执行此操作。具体来说,maps.frozenmap

1
2
3
import maps
fm = maps.FrozenMap(my_dict)
hash(fm)

要安装maps,只需执行以下操作:

1
pip install maps

它还处理嵌套的dict案例:

1
2
3
import maps
fm = maps.FrozenMap.recurse(my_dict)
hash(fm)

免责声明:我是maps图书馆的作者。


一般的方法很好,但是您可能需要考虑哈希方法。

sha是为加密强度(速度也是,但强度更重要)而设计的。您可能需要考虑到这一点。因此,使用内置的hash功能可能是一个好主意,除非这里的安全性是关键的。


我是这样做的:

1
hash(str(my_dict))