【问题标题】:Using non-hashable Python objects as keys in dictionaries使用不可散列的 Python 对象作为字典中的键
【发布时间】:2009-10-23 07:02:31
【问题描述】:

Python 不允许将不可散列的对象用作其他字典中的键。正如 Andrey Vlasovskikh 所指出的,对于使用非嵌套字典作为键的特殊情况,有一个很好的解决方法:

frozenset(a.items())#Can be put in the dictionary instead

有没有使用任意对象作为字典键的方法?

示例

这将如何用作密钥?

{"a":1, "b":{"c":10}}

您实际上必须在代码中使用类似的东西是非常罕见的。如果您认为是这种情况,请考虑先更改您的数据模型。

具体用例

用例是缓存对任意关键字函数的调用。字典中的每个键都是一个字符串(参数的名称),对象可能非常复杂,由分层字典、列表、元组等组成。

相关问题

这个子问题已从the problem here 中分离出来。这里的解决方案处理字典没有分层的情况。

【问题讨论】:

  • 如果没有人回答这个问题,那么我确实计划自己实现这个(参见 Chris Lutz 的解决方案)并将在此处发布解决方案。但是,请随时回答
  • 其实这个问题很讨厌。您几乎可以保证获得类型冲突,即。一种类型的字典与另一种不同:-(
  • 仅仅存储类的类型是行不通的,因为包含的类型也可能有类型:-(
  • -1:这不是“非常罕见”。这是不必要的。使用适当的__hash__ 函数创建您自己的适当类。 “任意”不应该进入对话。只需定义一个适当的类并消除此问题。
  • 我正在添加我的用例:一个在内存中执行的函数memoize。标准memoize 函数的问题是与数据库通信的开销。如果你在内存中使用memoize,当超时不是静态的时,没有简单的方法来清除缓存。

标签: python


【解决方案1】:

再次基于 Chris Lutz 的解决方案。

import collections

def hashable(obj):
    if isinstance(obj, collections.Hashable):
        items = obj
    elif isinstance(obj, collections.Mapping):
        items = frozenset((k, hashable(v)) for k, v in obj.iteritems())
    elif isinstance(obj, collections.Iterable):
        items = tuple(hashable(item) for item in obj)
    else:
        raise TypeError(type(obj))

    return items

【讨论】:

  • 这很好,但它不会捕获嵌套的 unhashable,例如a = {hashable(tuple(([1],[2]))):1} 失败。要修复它,您实际上需要 try 散列,如果它引发,则递归列表中的每个项目。具体来说,添加try: a = {items:1} except TypeError: return tuple(hashable(elem) for elem in items)
  • 对于 Python 3,将 iteritems 更改为 items
【解决方案2】:

不要。我同意 Andreys 对上一个问题的评论,即将字典作为键是没有意义的,尤其是嵌套字典。您的数据模型显然非常复杂,字典可能不是正确的答案。您应该尝试一些 OO。

【讨论】:

  • 我不同意它应该是评论。在我看来,这是正确的答案。当然是 YMMV。
  • +1:别这样。如果你认为你需要这个,那么你的“嵌套字典”不应该是一个嵌套字典——它应该是一个具有适当 __hash__ 方法的适当类。不要编写代码来处理“任意”结构。编写适当的类而不是“任意”结构。
  • 只要回答问题。另外:(1) 只有我使用的脚本中任意函数的 Memoize 装饰器是难以置信高效的。 (2) 它并不总是“我的数据模型”,并且添加必须将我所做的所有事情都包装在一个(可能是错误的)类中的障碍更加令人困惑。然而,说“如果你在我必须使用的代码中这样做,我会永远恨你”是公平的。 :)
【解决方案3】:

基于 Chris Lutz 的解决方案。请注意,这不处理迭代更改的对象,例如流,也不处理循环。

import collections

def make_hashable(obj):
    """WARNING: This function only works on a limited subset of objects
    Make a range of objects hashable. 
    Accepts embedded dictionaries, lists or tuples (including namedtuples)"""
    if isinstance(obj, collections.Hashable):
        #Fine to be hashed without any changes
        return obj
    elif isinstance(obj, collections.Mapping):
        #Convert into a frozenset instead
        items=list(obj.items())
        for i, item in enumerate(items):
                items[i]=make_hashable(item)
        return frozenset(items)
    elif isinstance(obj, collections.Iterable):
        #Convert into a tuple instead
        ret=[type(obj)]
        for i, item in enumerate(obj):
                ret.append(make_hashable(item))
        return tuple(ret)
    #Use the id of the object
    return id(obj)

【讨论】:

  • 与 Chris Lutz 的解决方案不同(没有冒犯 Chris),它确实有效。杰出的。使用 isinstance 来捕获子类的好处。
  • 一些不起作用的情况:i = []; i.append(i); make_hashable(i) # 最大递归; make_hashable({1:{}})
  • 也不会捕获嵌套的东西,例如a = {make_hashable(tuple(([1],[2]))):1} 失败。要修复它,您实际上需要 try 散列,如果是 raises,则递归列表中的每个项目。
【解决方案4】:

如果你真的必须,让你的对象可散列。子类化您想要作为键放入的任何内容,并提供一个 __hash__ 函数,该函数返回该对象的唯一键。

举例说明:

>>> ("a",).__hash__()
986073539
>>> {'a': 'b'}.__hash__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

如果您的哈希不够唯一,您将遇到冲突。也可能很慢。

【讨论】:

  • 一般来说不一定是最好的方法。就我而言,我从第 3 方图书馆取回字典。我必须将它们用作钥匙。我不能确保它们恰好对应于任何给定的类。我也不希望使用它们来创建临时对象来创建散列,因为它在这里是一次唯一的操作。根据上面 Chris Lutz 的解决方案,我宁愿调用一次 make_hashable,而不是乱用自定义类。
【解决方案5】:

我完全不同意 cmets 和回答说出于数据模型纯度的原因不应该这样做。

字典使用前一个对象作为键将一个对象与另一个对象相关联。字典不能用作键,因为它们不可散列。这不会降低将字典映射到其他对象的意义/实用性/必要性。

据我了解 Python 绑定系统,您可以将任何字典绑定到多个变量(或相反,取决于您的术语),这意味着这些变量都知道指向该字典的相同唯一“指针”。难道不能将该标识符用作散列键吗? 如果您的数据模型确保/强制您不能将两个具有相同内容的字典用作键,那么这对我来说似乎是一种安全的技术。

我应该补充一点,我不知道如何/应该如何做到这一点。

我并不完全认为这应该是一个答案还是一个评论。如有需要请指正。

【讨论】:

  • 谢谢,nekoniaow,现在将其合并到我的解决方案中。
  • 不要忘记:它们故意不可散列,因为它们是可变的。
  • 那又怎样?字典是从一个对象到另一个对象的关联映射。无论对象内容的可变性如何,您都可能需要此映射。如果 A 需要与 B 相关联,那么我们不需要关心 A 是否可以更改,映射不需要完全依赖于此。
【解决方案6】:

我同意 Lennart Regebro 的观点,你不同意。但是我经常发现缓存一些函数调用、可调用对象和/或享元对象很有用,因为它们可能使用关键字参数。

但如果你真的想要它,试试pickle.dumps(或者cPickle,如果是python 2.6)作为一个快速而肮脏的黑客。它比任何使用递归调用使项目不可变且字符串可散列的答案都快得多。

import pickle
hashable_str = pickle.dumps(unhashable_object)

【讨论】:

  • 这是一个完美的两行解决方案!我不同意这是一个肮脏的黑客。尽管在您的代码中使用 pickle 这个词很尴尬,但它比任何递归方法都要简单和清晰得多,它试图处理每种情况——但仍然不适用于复杂的输入。 pickle 根据定义几乎可以处理任何事情。您的代码注释可以简单地使用# magic hashifier,而不是试图解释所有可以散列和不能散列的变幻莫测,分布在 10 到 20 行之间。
【解决方案7】:

recursion!

def make_hashable(h):
    items = h.items()
    for item in items:
        if type(items) == dict:
            item = make_hashable(item)
    return frozenset(items)

您可以为任何其他想要使其可散列的可变类型添加其他类型测试。应该不难。

【讨论】:

  • 实际上,我认为它有点复杂是对的。需要特殊代码来处理元组、列表、集合......实际上可能需要花费大量精力来正确解决这个问题。我也应该重命名问题
  • 另外,我认为应该是 type(item)==dict
  • 另外将项目设置为 make_hashable(item) 不会在列表中设置它
  • 这是一个丑陋的 hack,分散了代码的含义。
【解决方案8】:

我在使用基于调用签名缓存先前调用结果的装饰器时遇到了这个问题。我不同意这里的 cmets/answers 大意是“你不应该这样做”,但我认为重要的是要认识到走这条路时可能会出现令人惊讶和意外的行为。我的想法是,由于实例既是可变的又是可散列的,并且改变它似乎不切实际,因此创建非散列类型或对象的可散列等效项本质上没有错。当然,这只是我的看法。

对于需要兼容 Python 2.5 的任何人,以下内容可能会有用。我基于之前的答案。

from itertools import imap
tuplemap = lambda f, data: tuple(imap(f, data))
def make_hashable(obj):
  u"Returns a deep, non-destructive conversion of given object to an equivalent hashable object"
  if isinstance(obj, list):
    return tuplemap(make_hashable, iter(obj))
  elif isinstance(obj, dict):
    return frozenset(tuplemap(make_hashable, obj.iteritems()))
  elif hasattr(obj, '__hash__') and callable(obj.__hash__):
    try:
      obj.__hash__()
    except:
      if hasattr(obj, '__iter__') and callable(obj.__iter__):
        return tuplemap(make_hashable, iter(obj))
      else:
        raise NotImplementedError, 'object of type %s cannot be made hashable' % (type(obj),)
    else:
      return obj
  elif hasattr(obj, '__iter__') and callable(obj.__iter__):
    return tuplemap(make_hashable, iter(obj))
  else:
    raise NotImplementedError, 'object of type %s cannot be made hashable' % (type, obj)

【讨论】:

  • 将最后的 raise 更改为“return obj”语句,以便它适用于实例。
猜你喜欢
  • 2021-12-17
  • 1970-01-01
  • 2013-12-05
  • 2013-08-23
  • 2010-11-12
  • 2011-10-01
  • 2011-10-09
  • 2011-11-25
  • 2023-03-27
相关资源
最近更新 更多