藏字典吗?


156

出于缓存目的,我需要根据字典中存在的GET参数生成缓存键。

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


4
这可能不适用于嵌套字典。最简单的解决方案是改用json.dumps(my_dict,sort_keys = True),它将递归为dict值。
Andrey Fedorov 2014年

2
仅供参考:dumps,stackoverflow.com /a/12739361/1082367说:“由于类似的原因,不能保证pickle的输出是规范的,并且决定顺序不确定,因此不要使用pickle或pprint或repr进行哈希处理”。
马修·康奈尔

排序字典键,而不是项目,我还将键发送给哈希函数。
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起),并且不依赖于dict的内部结构,如保留顺序(在python的整个生命周期中都在发展),并且如果两个数据结构在语义上相同(像{'a': 1, 'b':2}在语义上是相同的{'b':2, 'a':1})。我还没有用在任何过于复杂的事情上,所以YMMV但欢迎反馈。
奥利弗

Answers:


110

如果您的字典未嵌套,则可以使用字典的项目进行冻结设置,然后使用hash()

hash(frozenset(my_dict.items()))

与生成JSON字符串或字典表示相比,此方法的计算强度要​​低得多。

更新:请参阅下面的注释,为什么这种方法可能无法产生稳定的结果。


9
对于嵌套词典,这对我不起作用。我没有尝试下面的解决方案(太复杂了)。OP的解决方案运行良好。我用哈希替换了sha1以保存导入。
spatel 2012年

9
@Ceaser这是行不通的,因为元组暗示了排序,但是字典项没有排序。Frozenset更好。
锑2012年

28
如果某些东西需要在不同的机器上保持一致,请当心内置的哈希值。在诸如Heroku和GAE之类的云平台上执行python会在不同实例上返回hash()的不同值,从而使它对于必须在两个或更多“机器”之间共享的任何内容(对于heroku而言,是Dynos)都毫无用处
Ben Roberts

6
hash()函数不能产生稳定的输出可能会很有趣。这意味着,在给定相同输入的情况下,对于同一个python解释器的不同实例,它将返回不同的结果。在我看来,每次启动解释器时都会生成某种种子值。
Hermann Schachner,2015年

7
预期。出于安全原因,我记得添加了某种内存随机化,因此引入了种子。因此,您不能期望两个python进程之间的哈希值相同
Nikokrock,2015年

137

sorted(d.items())仅仅使用它不足以使我们保持稳定。其中的某些值d也可以是字典,并且它们的键仍将以任意顺序出现。只要所有键都是字符串,我更喜欢使用:

json.dumps(d, sort_keys=True)

就是说,如果散列需要在不同的机器或Python版本之间保持稳定,我不确定这是防弹的。您可能想要添加separatorsensure_ascii参数,以保护自己免受在那里对默认值的任何更改。我将不胜感激。


6
这只是偏执,但是JSON允许大多数字符以字符串形式显示而没有任何字符转义,因此编码器可以选择是否转义字符或仅将它们传递。这样做的风险是,默认情况下,编码器的不同版本(或将来的版本)可能会做出不同的转义选择,然后您的程序将在不同的环境中为同一字典计算不同的哈希值。该ensure_ascii论点可以防止这种完全假设的问题。
Jack O'Connor 2014年

4
我使用其他数据集测试了此性能,其速度比快得多make_hashgist.github.com/charlax/b8731de51d2ea86c6eb9
charlax

3
@charlax ujson不保证字典对的顺序,因此这样做并不安全
arthurprs 2015年

11
此解决方案仅在所有键都是字符串的情况下才有效,例如json.dumps({'a':{(0,5):5,1:1:3}})失败。
kadee

5
@LorenzoBelli,您可以通过添加default=strdumps命令中来克服这一点。似乎工作得很好。
mlissner '18

63

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

尽管已经接受了答案,但是问题的标题是“哈希Python字典”,并且关于该标题的答案不完整。(关于问题的内容,答案是完整的。)

嵌套词典

如果人们在Stack Overflow上搜索如何对字典进行哈希处理,则可能会偶然发现这个标题恰当的问题,并且如果人们试图对乘法嵌套字典进行哈希处理,则可能会感到不满意。上面的答案在这种情况下不起作用,您必须实现某种递归机制才能检索哈希。

这是一种这样的机制:

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()当您对类或实例进行哈希处理时,该函数非常有用。但是,这是我发现的与对象有关的哈希问题:

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

即使我更改了foo,哈希也一样。这是因为foo的标识未更改,因此哈希是相同的。如果希望foo根据其当前定义进行不同的哈希处理,则解决方案是对实际更改的内容进行哈希处理。在这种情况下,__dict__属性:

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

las,当您尝试对类本身执行相同的操作时:

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

class __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__]))

注意:以上所有代码均假定使用Python3.x。尽管我认为make_hash()可以在2.7.2中使用,但并未在早期版本中进行测试。至于使这些示例可行,我确实知道

func.__code__ 

应该替换为

func.func_code

isinstance使用第二个参数的序列,因此isinstance(o,(set,tuple,list))可以工作。
Xealot

感谢您使我意识到Frozenset可能会始终哈希查询字符串参数:)
Xealot13年

1
如果dict项顺序不同但键值不是,则需要对项进行排序以创建相同的哈希-> return hash(tuple(frozenset(sorted(new_o.items()))))
Bas Koopmans

真好!我还向hash周围的列表和元组添加了呼叫。否则,它将获取我的恰好是字典中值的整数列表,然后返回哈希表,这不是我想要的。
osa 2015年

冻结集是一个UNORDERED集合,因此对它的输入进行排序没有任何好处。另一方面,列表和元组是ORDERED集合(“序列”),因此哈希值应受其中项目顺序的影响。您不应该对它们进行排序!
RobM '16

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 hash()函数,因为它不会提供在Python重新启动后保持一致的哈希值(请参阅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不支持“设置”表示形式。
Suraj

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照顾了这一点
glarrain 2014年

2
根据定义,集合是无序的。因此,添加对象的顺序无关紧要。您必须认识到内置函数hash并不关心如何打印frozenset内容或类似内容。在多台机器和python版本中对其进行测试,您将看到。
glarrain 2014年

为什么在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模块冻结字典并使其可哈希化。

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对象。
詹姆斯·希尔斯霍恩

@JamesHirschorn:更新为大声失败
Eric

更好!我添加了以下elif子句以使其与DataFrames一起使用: elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) 我将编辑答案,看看是否接受...
James Hirschorn

1
好。我看到我要求的不仅仅是“可散列”,这只能保证相等的对象具有相同的散列。我工作的一个版本,这将使运行之间的相同值,以及独立的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应该会有所帮助,但我想知道如何使用包含字典列表的嵌套字典来处理该案件。如字典是无法散列的。
Suraj

1
@Suraj:它确实通过处理嵌套结构.recurse。请参阅maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse。列表中的排序在语义上是有意义的,如果您希望顺序独立,则可以在调用之前将列表转换为集合.recurse。您还可以使用该list_fn参数来.recurse使用与tuplefrozenset
.eg

0

解决该问题的一种方法是对字典中的项进行元组化:

hash(tuple(my_dict.items()))

-8

我这样做是这样的:

hash(str(my_dict))

1
有人可以解释这种方法有什么问题吗?
mhristache '16

7
@maximi字典的顺序条款不稳定,因此hash(str({'a': 1, 'b': 2})) != hash(str({'b': 2, 'a': 1}))(尽管它可能适用于某些词典,但不能保证对所有词典都适用)。
弗拉德·弗罗洛夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.