【问题标题】:Hashing a dictionary?散列字典?
【发布时间】:2011-08-18 13:12:24
【问题描述】:

出于缓存目的,我需要从 dict 中存在的 GET 参数生成缓存键。

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

【问题讨论】:

  • 这可能不适用于嵌套字典。最短的解决方案是使用 json.dumps(my_dict, sort_keys=True) 代替,它将递归到 dict 值。
  • 仅供参考:转储,stackoverflow.com/a/12739361/1082367 说“由于与 dict 和 set order 不确定的类似原因,无法保证 pickle 的输出是规范的。不要使用 pickle 或 pprint 或 repr用于散列。”
  • 排序字典键,而不是项目,我也会将键发送到散列函数。
  • 关于散列可变数据结构(如字典)的有趣背景故事:python.org/dev/peps/pep-0351 被提议允许任意冻结对象,但被拒绝。有关基本原理,请参阅 python-dev 中的此线程:mail.python.org/pipermail/python-dev/2006-February/060793.html
  • 如果您的数据是 json 格式,并且您想要语义不变的散列,请查看 github.com/schollii/sandals/blob/master/json_sem_hash.py。它适用于嵌套结构(当然,从 json 开始),并且不依赖于 dict 的内部结构,如保留顺序(在 python 的整个生命周期中已经演变),并且如果两个数据结构在语义上相同(像{'a': 1, 'b':2} 在语义上与{'b':2, 'a':1} 相同)。我还没有在任何太复杂的东西上使用它,所以 YMMV 但欢迎反馈。

标签: python hash dictionary


【解决方案1】:

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

json.dumps(d, sort_keys=True)

也就是说,如果哈希需要在不同的机器或 Python 版本之间保持稳定,我不确定这是否是万无一失的。您可能想要添加 separatorsensure_ascii 参数以保护自己免受那里默认值的任何更改。我会很感激 cmets。

【讨论】:

  • 这只是偏执,但 JSON 允许大多数字符显示在字符串中而无需任何文字转义,因此编码器可以选择是否转义字符或只是传递它们。那么风险是编码器的不同版本(或未来版本)默认情况下可能会做出不同的转义选择,然后您的程序将在不同环境中为同一字典计算不同的哈希值。 ensure_ascii 参数可以防止这个完全假设的问题。
  • 我用不同的数据集测试了它的性能,它比make_hash快得多。 gist.github.com/charlax/b8731de51d2ea86c6eb9
  • @charlax ujson 不保证 dict 对的顺序,所以这样做是不安全的
  • 此解决方案仅在所有键都是字符串时才有效,例如json.dumps({'a': {(0, 5): 5, 1: 3}}) 失败。
  • @LorenzoBelli,您可以通过将default=str 添加到dumps 命令来克服这个问题。似乎工作得很好。
【解决方案2】:

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

hash(frozenset(my_dict.items()))

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

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

【讨论】:

  • 这不适用于嵌套字典。我还没有尝试过下面的解决方案(太复杂了)。 OP 的解决方案工作得非常好。我用哈希替换了 sha1 以保存导入。
  • @Ceaser 这行不通,因为 tuple 意味着排序但 dict 项目是无序的。 freezeset 更好。
  • 如果需要在不同机器上保持一致,请注意内置哈希。在 Heroku 和 GAE 等云平台上实现 python 将在不同实例上为 hash() 返回不同的值,这使得它对于必须在两个或多个“机器”之间共享的任何东西都无用(在 heroku 的情况下为 dynos)
  • 有趣的是,hash() 函数不会产生稳定的输出。这意味着,给定相同的输入,它会使用相同 python 解释器的不同实例返回不同的结果。在我看来,每次启动解释器时都会生成某种种子值。
  • 预计。就我记得添加某种内存随机化而言,出于安全原因引入了种子。所以你不能指望两个python进程之间的哈希值是一样的
【解决方案3】:

编辑:如果你所有的键都是字符串,那么在继续阅读这个答案之前,请参阅 Jack O'Connor 的显着simpler (and faster) solution(它也适用于散列嵌套字典)。

虽然答案已被接受,但问题的标题是“Hashing a python dictionary”,并且该标题的答案不完整。 (关于问题的主体,答案是完整的。)

嵌套字典

如果您在 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

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

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 为第二个参数采用序列,因此 isinstance(o, (set, tuple, list)) 可以工作。
  • 感谢您让我意识到frozenset 可以始终如一地散列查询字符串参数:)
  • 如果dict项目顺序不同但键值不同,则需要对项目进行排序以创建相同的哈希-> return hash(tuple(frozenset(sorted(new_o.items( )))))
  • 不错!我还围绕列表和元组添加了对hash 的调用。否则,它将获取我的字典中恰好是值的整数列表,并返回哈希列表,这不是我想要的。
  • 在 Python 3.7 中,此解决方案的性能比 hash(json.dumps(d, sort_keys=True)) 差 18 倍。证明:pastebin.com/XDZGhNHs
【解决方案4】:

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

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)):,那么这个函数可以作用于任何对象。
【解决方案5】:

下面的代码避免使用 Python hash() 函数,因为它不会提供在 Python 重新启动时一致的哈希(请参阅hash function in Python 3.3 returns different results between sessions)。 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=

【讨论】:

  • 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__ 添加到每个元组的开头以强制区分。
  • 如果你也想对列表进行排序:tuple(sorted((make_hashable(e) for e in o)))
  • make_hash_sha256() - 不错!
  • @Suraj 您不应该在散列之前对列表进行排序,因为其内容以不同顺序排列的列表绝对不是一回事。如果项目的顺序无关紧要,问题是您使用了错误的数据结构。您应该使用集合而不是列表。
  • @scottclowe 确实如此。感谢您添加这一点。在 2 种情况下,您仍然需要一个列表(没有特定的订购需求) - 1. 重复项目列表。 2. 当你必须直接使用 JSON 时。由于 JSON 不支持“设置”表示。
【解决方案6】:

MD5 哈希

对我来说结果最稳定的方法是使用 md5 哈希和 json.stringify

from typing import Dict, Any
import hashlib
import json

def dict_hash(dictionary: Dict[str, Any]) -> str:
    """MD5 hash of a dictionary."""
    dhash = hashlib.md5()
    # We need to sort arguments so {'a': 1, 'b': 2} is
    # the same as {'b': 2, 'a': 1}
    encoded = json.dumps(dictionary, sort_keys=True).encode()
    dhash.update(encoded)
    return dhash.hexdigest()

【讨论】:

    【解决方案7】:

    虽然hash(frozenset(x.items())hash(tuple(sorted(x.items())) 工作,但分配和复制所有键值对的工作量很大。哈希函数确实应该避免大量内存分配。

    这里有一点数学知识。大多数散列函数的问题在于它们假设顺序很重要。要散列无序结构,您需要一个交换操作。乘法不能很好地工作,因为任何散列为 0 的元素都意味着整个乘积为 0。按位 &amp;| 倾向于全 0 或 1。有两个很好的候选:加法和异或。

    from functools import reduce
    from operator import xor
    
    class hashable(dict):
        def __hash__(self):
            return reduce(xor, map(hash, self.items()), 0)
    
        # Alternative
        def __hash__(self):
            return sum(map(hash, self.items()))
    

    一点:xor 起作用,部分原因是dict 保证键是唯一的。并且 sum 有效,因为 Python 会按位截断结果。

    如果你想散列一个多重集,sum 是更可取的。使用异或,{a} 将哈希到与{a, a, a} 相同的值,因为x ^ x ^ x = x

    如果您确实需要 SHA 提供的保证,那么这对您不起作用。但是要在集合中使用字典,这会很好; Python 容器可以抵抗一些冲突,并且底层的哈希函数非常好。

    【讨论】:

      【解决方案8】:

      从 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 负责处理
      • 集合,根据定义,是无序的。因此,添加对象的顺序是无关紧要的。您必须意识到内置函数hash 并不关心frozenset 内容的打印方式或类似内容。在几台机器和python版本上测试一下,你就会看到。
      • 为什么在 value = hash('%s::%s' % (value, type(value))) 中使用额外的 hash() 调用??
      【解决方案9】:

      为了保留密钥顺序,而不是hash(str(dictionary))hash(json.dumps(dictionary)),我更喜欢快速而肮脏的解决方案:

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

      它甚至适用于像 DateTime 这样的类型以及更多不可 JSON 序列化的类型。

      【讨论】:

      • 谁保证 pformat 或 json 总是使用相同的顺序?
      • @ThiefMaster, "2.5 版更改:字典在计算显示之前按键排序;在 2.5 之前,字典仅在其显示需要多行时才进行排序,尽管这不是记录在案。”(docs.python.org/2/library/pprint.html)
      • 这对我来说似乎无效。作者将 pprint 模块和 pformat 理解为用于显示目的而不是序列化。因此,假设 pformat 将始终返回恰好有效的结果是不安全的。
      【解决方案10】:

      您可以使用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 库的作者。

      【讨论】:

      • 库不对字典中的列表进行排序。因此这可能会产生不同的哈希值。也应该有一个对列表进行排序的选项。 freezeset 应该会有所帮助,但我想知道您将如何处理包含 dicts 列表的嵌套 dict 的情况。因为 dict 是不可散列的。
      • @Suraj :它确实通过.recurse处理嵌套结构。见maps.readthedocs.io/en/latest/api.html#maps.FrozenMap.recurse。列表中的排序在语义上是有意义的,如果您想要顺序独立,您可以在调用 .recurse 之前将列表转换为集合。您还可以使用.recurselist_fn 参数来使用与tuple 不同的可散列数据结构(例如frozenset
      【解决方案11】:

      您可以使用第三方 frozendict module 冻结您的字典并使其可散列。

      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:更新为大声失败
      • 更好!我添加了以下elif 子句以使其与DataFrames 一起使用:elif isinstance(x, pd.DataFrame): return make_hashable(hash_pandas_object(x).tolist()) 我将编辑答案,看看您是否接受...
      • 好的。我看到我要求的不仅仅是“可散列”,它只保证相等的对象应该具有相同的散列。我正在开发一个版本,它将在运行之间提供相同的值,并且独立于 python 版本等。
      • hash 随机化是在 python 3.7 中默认启用的故意安全功能。
      【解决方案12】:

      这个帖子中获得最高支持的答案对我不起作用,因为由于PYTHOPYTHONHASHSEED,他们的哈希函数在不同的机器上给出了不同的结果。

      我调整了这个帖子中的所有提示,并提出了一个适合我的解决方案。

      import collections
      import hashlib
      import json
      
      
      def simplify_object(o):
          if isinstance(o, dict):
              ordered_dict = collections.OrderedDict(sorted(o.items()))
              for k, v in ordered_dict.items():
                  v = simplify_object(v)
                  ordered_dict[str(k)] = v
              o = ordered_dict
          elif isinstance(o, (list, tuple, set)):
              o = [simplify_object(el) for el in o]
          else:
              o = str(o).strip()
          return o
      
      
      def make_hash(o):
          o = simplify_object(o)
          bytes_val = json.dumps(o, sort_keys=True, ensure_ascii=True, default=str)
          hash_val = hashlib.sha1(bytes_val.encode()).hexdigest()
          return hash_val
      

      【讨论】:

      • 然后设置 PYTHONHASHSEED=0,如果你可以访问环境的话。
      • @marco 好吧,这听起来不像是任何人想要的。 > 指定值 0 将禁用哈希随机化。
      • 它不会禁用哈希随机化,只是种子随机化,这是在不同机器上获得相同结果的随机化的唯一方法,你说这就是你想要实现的(也许我误解了你)。无论如何,两者似乎都与原始问题没有太大关系。
      【解决方案13】:

      解决该问题的一种方法是创建字典项的元组:

      hash(tuple(my_dict.items()))
      

      【讨论】:

        【解决方案14】:

        使用来自DeepDiff模块的DeepHash

        from deepdiff import DeepHash
        obj = {'a':'1',b:'2'}
        hashes = DeepHash(obj)[obj]
        

        【讨论】:

          【解决方案15】:

          我是这样做的:

          hash(str(my_dict))
          

          【讨论】:

          • 有人能解释一下这种方法有什么问题吗?
          • 对于我的用例来说已经足够了
          猜你喜欢
          • 2020-09-05
          • 2010-11-12
          • 1970-01-01
          • 2020-11-08
          • 2013-12-05
          • 1970-01-01
          • 1970-01-01
          • 2016-09-03
          • 2015-12-23
          相关资源
          最近更新 更多