Python可sorting的字典

作为一个练习,主要是为了我自己的娱乐,我正在实施一个回溯packratparsing器。 灵感来源于此,我希望能够更好地了解hygenicmacros如何在algol-like语言中工作(就像你通常在其中find的没有语法的lisp方言一样)。 因此,input的不同传递可能会看到不同的语法,所以caching的parsing结果是无效的,除非我还将当前版本的语法与caching的parsing结果一起存储。 ( 编辑 :键值集合的这种使用的结果是它们应该是不可变的,但我不打算暴露接口,以允许它们被改变,所以无论是可变的或不可变的集合都可以)

问题是python dicts不能作为其他字典的键出现。 即使使用元组(反正我会这样做)没有帮助。

>>> 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'} 

好。 但是我必须为我想要使用的规则中的每个可能的键组合创build一个类,这不是那么糟糕,因为每个parsing规则确切地知道它使用的是什么参数,所以可以同时定义类作为parsing规则的函数。

编辑:与namedtuple s的另一个问题是,他们是严格的位置。 两个看起来应该不同的元组实际上可以是相同的:

 >>> 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 

tl'dr:如何获得可以用作其他dict的键的dict

攻击了一些答案,这里是我使用的更完整的解决scheme。 请注意,这做了一些额外的工作,使实际的目的隐约不可改变的结果。 当然,通过调用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() 

这是制作可sorting词典的简单方法。 只要记住不要因为显而易见的原因embedded到另一个字典中而进行变异。

 class hashabledict(dict): def __hash__(self): return hash(tuple(sorted(self.items()))) 

Hashables应该是不可变的 – 不是强制执行,而是TRUSTING在第一次使用密钥之后不要改变字典,下面的方法是可行的:

 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() 

如果你需要改变你的字典,还想把它们当作钥匙,那么复杂程度会爆发百倍 – 但并不是说它不能完成,但是在我进入那个令人难以置信的沼泽之前,我会等到一个非常具体的指示! – )

所有需要使字典可用于您的目的是添加一个__hash__方法:

 class Hashabledict(dict): def __hash__(self): return hash(frozenset(self)) 

请注意, 冷凝集转换将适用于所有字典(即不要求键可以sorting)。 同样,对字典值没有限制。

如果有许多字典具有相同的密钥但具有不同的值,则有必要使散列考虑这些值。 最快的方法是:

 class Hashabledict(dict): def __hash__(self): return hash((frozenset(self), frozenset(self.itervalues()))) 

这比frozenset(self.iteritems())更快,原因有两个。 首先, frozenset(self)步骤重用存储在字典中的散列值,将不必要的调用保存为hash(key) 。 其次,使用itervalues将直接访问这些值,避免每次执行查找时,通过项目使用许多内存分配器调用来在内存中形成新的许多键/值元组。

给定的答案是可以的,但是可以通过使用frozenset(...)而不是tuple(sorted(...))来生成哈希:

 >>> import timeit >>> timeit.timeit('hash(tuple(sorted(d.iteritems())))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')") 4.7758948802947998 >>> timeit.timeit('hash(frozenset(d.iteritems()))', "d = dict(a=3, b='4', c=2345, asdfsdkjfew=0.23424, x='sadfsadfadfsaf')") 1.8153600692749023 

性能优势取决于字典的内容,但在大多数情况下,我已经testing过,使用frozenset散列速度至less快了2倍(主要是因为它不需要sorting)。

一个相当干净,直接的实现是

 import collections class FrozenDict(collections.Mapping): """Don't forget the docstrings!!""" def __init__(self, *args, **kwargs): self._d = dict(*args, **kwargs) def __iter__(self): return iter(self._d) def __len__(self): return len(self._d) def __getitem__(self, key): return self._d[key] def __hash__(self): return hash(tuple(sorted(self._d.iteritems()))) 

我不断回到这个话题…这是另一个变化。 我不喜欢子类添加一个__hash__方法; 字典是可变的,而且相信他们不会改变的问题似乎是一个弱点。 所以我反而看着build立一个基于内buildtypes的映射本身是不可变的。 虽然tuple是一个明显的select,但是访问它的值意味着一种sorting和一个对分; 不是一个问题,但它似乎并没有利用它所build立的types的大部分力量。

如果你frozenset键值对变成一个frozenset呢? 那需要什么,它将如何工作?

第一部分,你需要一种编码'项目的方式,这样一个冷冻集将主要通过它们的键来对待它们。 我会为此做一个小类。

 import collections class pair(collections.namedtuple('pair_base', 'key value')): def __hash__(self): return hash((self.key, None)) def __eq__(self, other): if type(self) != type(other): return NotImplemented return self.key == other.key def __repr__(self): return repr((self.key, self.value)) 

仅凭这一点,你就可以随心所欲地映射出一个距离:

 >>> frozenset(pair(k, v) for k, v in enumerate('abcd')) frozenset([(0, 'a'), (2, 'c'), (1, 'b'), (3, 'd')]) >>> pairs = frozenset(pair(k, v) for k, v in enumerate('abcd')) >>> pair(2, None) in pairs True >>> pair(5, None) in pairs False >>> goal = frozenset((pair(2, None),)) >>> pairs & goal frozenset([(2, None)]) 

D'哦! 不幸的是,当你使用set操作符和元素相等但不相同的对象时, 哪一个最终返回值是不确定的 ,我们将不得不经过更多的回旋。

 >>> pairs - (pairs - goal) frozenset([(2, 'c')]) >>> iter(pairs - (pairs - goal)).next().value 'c' 

然而,以这种方式来看待价值是麻烦的,而且更糟糕的是,创造了大量的中间集合; 那不行! 我们将创build一个“假”的键值对来解决它:

 class Thief(object): def __init__(self, key): self.key = key def __hash__(self): return hash(pair(self.key, None)) def __eq__(self, other): self.value = other.value return pair(self.key, None) == other 

这导致问题较less:

 >>> thief = Thief(2) >>> thief in pairs True >>> thief.value 'c' 

这就是深奥的魔力; 剩下的就是将它们全部包装成一个像dict一样的界面 。 由于我们从具有非常不同的接口的frozenset ,所以有相当多的方法。 我们从collections.Mapping获得了一些帮助,但是大多数工作都覆盖了frozenset一样工作的版本的frozenset方法,而不是:

 class FrozenDict(frozenset, collections.Mapping): def __new__(cls, seq=()): return frozenset.__new__(cls, (pair(k, v) for k, v in seq)) def __getitem__(self, key): thief = Thief(key) if frozenset.__contains__(self, thief): return thief.value raise KeyError(key) def __eq__(self, other): if not isinstance(other, FrozenDict): return dict(self.iteritems()) == other if len(self) != len(other): return False for key, value in self.iteritems(): try: if value != other[key]: return False except KeyError: return False return True def __hash__(self): return hash(frozenset(self.iteritems())) def get(self, key, default=None): thief = Thief(key) if frozenset.__contains__(self, thief): return thief.value return default def __iter__(self): for item in frozenset.__iter__(self): yield item.key def iteritems(self): for item in frozenset.__iter__(self): yield (item.key, item.value) def iterkeys(self): for item in frozenset.__iter__(self): yield item.key def itervalues(self): for item in frozenset.__iter__(self): yield item.value def __contains__(self, key): return frozenset.__contains__(self, pair(key, None)) has_key = __contains__ def __repr__(self): return type(self).__name__ + (', '.join(repr(item) for item in self.iteritems())).join('()') @classmethod def fromkeys(cls, keys, value=None): return cls((key, value) for key in keys) 

这最终确实回答了我自己的问题:

 >>> myDict = {} >>> myDict[FrozenDict(enumerate('ab'))] = 5 >>> FrozenDict(enumerate('ab')) in myDict True >>> FrozenDict(enumerate('bc')) in myDict False >>> FrozenDict(enumerate('ab', 3)) in myDict False >>> myDict[FrozenDict(enumerate('ab'))] 5 

您可能还需要添加这两种方法来使v2 pickling协议与hashdict实例一起工作。 否则cPickle会尝试使用hashdict .____ setitem____,导致TypeError。 有趣的是,与协议的其他两个版本,你的代码工作得很好。

 def __setstate__(self, objstate): for k,v in objstate.items(): dict.__setitem__(self,k,v) def __reduce__(self): return (hashdict, (), dict(self),) 

@Unknown接受的答案,以及@AlexMartelli的答案工作得很好,但只有在以下约束下:

  1. 字典的值必须是可散列的。 例如, hash(hashabledict({'a':[1,2]}))会引发TypeError
  2. 密钥必须支持比较操作。 例如, hash(hashabledict({'a':'a', 1:1}))会引发TypeError
  3. 按键上的比较运算符强加总sorting。 例如,如果一个字典中的两个键都被frozenset((1,2,3))frozenset((4,5,6)) ,那么它们在两个方向上都是不相等的。 因此,使用这些键对字典的项进行sorting可能导致任意顺序,因此违反了相等对象必须具有相同散列值的规则。

@ObenSonne更快的答案解除了约束2和约束3,但仍然受限于约束1(值必须是可散列的)。

@RaymondHettinger的答案更快,但却提升了所有3个约束,因为它不包含散列计算中的.values() 。 但是,只有在以下情况下,

  1. 大多数需要被散列的(不相等的)字典都不相同.keys()

如果这个条件不满足,散列函数仍然有效,但是可能导致太多的冲突。 例如,在所有字典都是从网站模板生成(字段名称为键,用户input为值)的极端情况下,键将始终保持不变,散列函数将为所有input返回相同的值。 因此,依赖于这种散列函数的散列表将在检索项( O(N)而不是O(1) )时变得像列表一样慢。

我认为即使我上面列出的所有4个约束条件都被违反了,下面的解决scheme也能很好地工作。 它有一个额外的好处,即它可以散列字典,但任何容器,即使他们有嵌套可变容器。

我非常感谢这方面的任何反馈意见,因为我只对这一点进行了轻微的testing。

 # python 3.4 import collections import operator import sys import itertools import reprlib # a wrapper to make an object hashable, while preserving equality class AutoHash: # for each known container type, we can optionally provide a tuple # specifying: type, transform, aggregator # even immutable types need to be included, since their items # may make them unhashable # transformation may be used to enforce the desired iteration # the result of a transformation must be an iterable # default: no change; for dictionaries, we use .items() to see values # usually transformation choice only affects efficiency, not correctness # aggregator is the function that combines all items into one object # default: frozenset; for ordered containers, we can use tuple # aggregator choice affects both efficiency and correctness # eg, using a tuple aggregator for a set is incorrect, # since identical sets may end up with different hash values # frozenset is safe since at worst it just causes more collisions # unfortunately, no collections.ABC class is available that helps # distinguish ordered from unordered containers # so we need to just list them out manually as needed type_info = collections.namedtuple( 'type_info', 'type transformation aggregator') ident = lambda x: x # order matters; first match is used to handle a datatype known_types = ( # dict also handles defaultdict type_info(dict, lambda d: d.items(), frozenset), # no need to include set and frozenset, since they are fine with defaults type_info(collections.OrderedDict, ident, tuple), type_info(list, ident, tuple), type_info(tuple, ident, tuple), type_info(collections.deque, ident, tuple), type_info(collections.Iterable, ident, frozenset) # other iterables ) # hash_func can be set to replace the built-in hash function # cache can be turned on; if it is, cycles will be detected, # otherwise cycles in a data structure will cause failure def __init__(self, data, hash_func=hash, cache=False, verbose=False): self._data=data self.hash_func=hash_func self.verbose=verbose self.cache=cache # cache objects' hashes for performance and to deal with cycles if self.cache: self.seen={} def hash_ex(self, o): # note: isinstance(o, Hashable) won't check inner types try: if self.verbose: print(type(o), reprlib.repr(o), self.hash_func(o), file=sys.stderr) return self.hash_func(o) except TypeError: pass # we let built-in hash decide if the hash value is worth caching # so we don't cache the built-in hash results if self.cache and id(o) in self.seen: return self.seen[id(o)][0] # found in cache # check if o can be handled by decomposing it into components for typ, transformation, aggregator in AutoHash.known_types: if isinstance(o, typ): # another option is: # result = reduce(operator.xor, map(_hash_ex, handler(o))) # but collisions are more likely with xor than with frozenset # eg hash_ex([1,2,3,4])==0 with xor try: # try to frozenset the actual components, it's faster h = self.hash_func(aggregator(transformation(o))) except TypeError: # components not hashable with built-in; # apply our extended hash function to them h = self.hash_func(aggregator(map(self.hash_ex, transformation(o)))) if self.cache: # storing the object too, otherwise memory location will be reused self.seen[id(o)] = (h, o) if self.verbose: print(type(o), reprlib.repr(o), h, file=sys.stderr) return h raise TypeError('Object {} of type {} not hashable'.format(repr(o), type(o))) def __hash__(self): return self.hash_ex(self._data) def __eq__(self, other): # short circuit to save time if self is other: return True # 1) type(self) a proper subclass of type(other) => self.__eq__ will be called first # 2) any other situation => lhs.__eq__ will be called first # case 1. one side is a subclass of the other, and AutoHash.__eq__ is not overridden in either # => the subclass instance's __eq__ is called first, and we should compare self._data and other._data # case 2. neither side is a subclass of the other; self is lhs # => we can't compare to another type; we should let the other side decide what to do, return NotImplemented # case 3. neither side is a subclass of the other; self is rhs # => we can't compare to another type, and the other side already tried and failed; # we should return False, but NotImplemented will have the same effect # any other case: we won't reach the __eq__ code in this class, no need to worry about it if isinstance(self, type(other)): # identifies case 1 return self._data == other._data else: # identifies cases 2 and 3 return NotImplemented d1 = {'a':[1,2], 2:{3:4}} print(hash(AutoHash(d1, cache=True, verbose=True))) d = AutoHash(dict(a=1, b=2, c=3, d=[4,5,6,7], e='a string of chars'),cache=True, verbose=True) print(hash(d)) 

如果你不把数字放在字典里,而且你永远不会丢失包含你的字典的variables,你可以这样做:

cache[id(rule)] = "whatever"

因为id()对每个字典都是唯一的

编辑:

哦,对不起,在那种情况下,其他人说的会更好。 我想你也可以把你的字典串行化为一个string

cache[ 'foo:bar' ] = 'baz'

如果你需要从键盘上恢复你的字典,那么你必须做更丑陋的事情

cache[ 'foo:bar' ] = ( {'foo':'bar'}, 'baz' )

我想这样做的好处是你不必编写太多的代码。