可哈希的字典(dicts)在Python中
作为一个练习,主要是为了我自己的乐趣,我正在实现一个回溯的“打包鼠”解析器。这个想法的灵感来源于我想更好地理解在类似Algol的语言中,卫生宏是如何工作的(而不是通常在没有语法的Lisp方言中找到的那种)。因此,对输入的不同处理可能会看到不同的语法,所以缓存的解析结果是无效的,除非我同时存储当前语法的版本和缓存的解析结果。(编辑:使用键值集合的一个结果是它们应该是不可变的,但我不打算暴露接口让它们可以被改变,所以可变或不可变的集合都可以)
问题是,Python的字典不能作为其他字典的键。即使使用元组(反正我也会这样做)也没有帮助。
>>> cache = {}
>>> rule = {"foo":"bar"}
>>> cache[(rule, "baz")] = "quux"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'
>>>
我想这必须一直用元组。现在,Python标准库提供了大致我需要的东西,collections.namedtuple
有着非常不同的语法,但可以作为键使用。接着上面的讨论:
>>> from collections import namedtuple
>>> Rule = namedtuple("Rule",rule.keys())
>>> cache[(Rule(**rule), "baz")] = "quux"
>>> cache
{(Rule(foo='bar'), 'baz'): 'quux'}
好的。但我必须为我想使用的规则中每种可能的键组合创建一个类,这并不是太糟糕,因为每个解析规则确切知道它使用了哪些参数,所以这个类可以在解析规则的函数同时定义。
编辑:namedtuple
的另一个问题是它们是严格按位置的。看起来应该不同的两个元组实际上可能是相同的:
>>> you = namedtuple("foo",["bar","baz"])
>>> me = namedtuple("foo",["bar","quux"])
>>> you(bar=1,baz=2) == me(bar=1,quux=2)
True
>>> bob = namedtuple("foo",["baz","bar"])
>>> you(bar=1,baz=2) == bob(bar=1,baz=2)
False
总结一下:我该如何获得可以作为其他dict
键的dict
?
在对答案进行了一些修改后,这里是我正在使用的更完整的解决方案。请注意,这做了一些额外的工作,使得结果字典在实际使用中大致是不可变的。当然,通过调用dict.__setitem__(instance, key, value)
来绕过这一点仍然很简单,但我们都是成年人了。
class hashdict(dict):
"""
hashable dict implementation, suitable for use as a key into
other dicts.
>>> h1 = hashdict({"apples": 1, "bananas":2})
>>> h2 = hashdict({"bananas": 3, "mangoes": 5})
>>> h1+h2
hashdict(apples=1, bananas=3, mangoes=5)
>>> d1 = {}
>>> d1[h1] = "salad"
>>> d1[h1]
'salad'
>>> d1[h2]
Traceback (most recent call last):
...
KeyError: hashdict(bananas=3, mangoes=5)
based on answers from
http://stackoverflow.com/questions/1151658/python-hashable-dicts
"""
def __key(self):
return tuple(sorted(self.items()))
def __repr__(self):
return "{0}({1})".format(self.__class__.__name__,
", ".join("{0}={1}".format(
str(i[0]),repr(i[1])) for i in self.__key()))
def __hash__(self):
return hash(self.__key())
def __setitem__(self, key, value):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def __delitem__(self, key):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def clear(self):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def pop(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def popitem(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def setdefault(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
def update(self, *args, **kwargs):
raise TypeError("{0} does not support item assignment"
.format(self.__class__.__name__))
# update is not ok because it mutates the object
# __add__ is ok because it creates a new object
# while the new object is under construction, it's ok to mutate it
def __add__(self, right):
result = hashdict(self)
dict.update(result, right)
return result
if __name__ == "__main__":
import doctest
doctest.testmod()
11 个回答
为了让字典可以满足你的需求,只需要添加一个叫做 __hash__ 的方法:
class Hashabledict(dict):
def __hash__(self):
return hash(frozenset(self))
需要注意的是,frozenset 转换对所有字典都有效(也就是说,键不需要是可排序的)。同样,字典的值也没有任何限制。
如果有很多字典,它们的键是一样的,但值不同,那么在计算哈希值的时候就需要考虑这些值。最快的方法是:
class Hashabledict(dict):
def __hash__(self):
return hash((frozenset(self), frozenset(self.itervalues())))
这个方法比 frozenset(self.iteritems())
快,原因有两个。首先,frozenset(self)
这一步会重用字典中存储的哈希值,这样就省去了不必要的 hash(key)
调用。其次,使用 itervalues 可以直接访问值,避免了每次查找时使用 items 创建新的键/值元组而导致的多次内存分配调用。
哈希对象应该是不可变的——这不是强制要求,而是相信你在第一次使用字典作为键之后不会去改变它。如果你遵循这个原则,下面的方法是可行的:
class hashabledict(dict):
def __key(self):
return tuple((k,self[k]) for k in sorted(self))
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return self.__key() == other.__key()
如果你确实需要修改你的字典,并且还想把它用作键,那就会变得非常复杂,复杂度会增加几百倍——这并不是说完全不可能做到,但在我看到非常明确的指示之前,我是不会去深入探讨这个复杂的问题的!-)
这里有一个简单的方法来创建一个可以被哈希的字典。只要记住,在把它放进另一个字典后,不要去改变它,原因很明显。
class hashabledict(dict):
def __hash__(self):
return hash(tuple(sorted(self.items())))