Python哈希值字典


91

作为一种练习,并且主要是出于我的娱乐,我正在实现回溯packrat解析器。这样做的灵感是,我想更好地了解Hygenic宏如何在类似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'}

好。但是我必须为我想使用的规则中的每个可能的键组合创建一个类,这并不坏,因为每个解析规则都确切知道它使用的参数,因此可以同时定义该类。作为解析规则的函数。

编辑:namedtuples 的另一个问题是它们严格处于位置。看起来应该不同的两个元组实际上可以相同:

>>> 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可以用作其他dicts的键的s?

在回答了一些问题之后,这是我正在使用的更完整的解决方案。请注意,这做了一些额外的工作,以使所得到的指示对于实际目的几乎不可变。当然,通过致电解决它仍然很容易,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()

hashdict必须是不可变的,至少你开始哈希处理后,为什么不缓存keyhash值作为属性的hashdict对象?我修改了__key()__hash__(),并进行测试以确认它的速度要快得多。因此,不允许在注释中使用格式化的代码,因此我将在此处链接它:sam.aiki.info/hashdict.py
Sam Watkins

Answers:


67

这是制作可哈希字典的简单方法。请记住,出于明显的原因,不要在将它们嵌入另一本词典后对其进行变异。

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

7
这不能完全确保eqhash的一致性,而我先前的回答是通过使用__key方法来实现的(实际上,这两种方法都应该起作用,尽管通过创建不需要的迭代列表可能会减慢该方法的工作-可通过s / items修复) / iteritems /-假设您没有说Python 2. * ;-)。
Alex Martelli,2009年

5
最好只使用Frozenset而不是使用tuple进行排序。这不仅会更快,而且您不能假定字典键具有可比性。
asmeurer,2012年

1
这似乎应该是避免哈希函数,这是一个方式O(n*log(n)),其中n是多少dict条目。有谁知道Python的frozenset哈希函数是否在线性时间内运行?
Tom Karzes

2
@HelloGoodbye也可以这样dict(key1=value1, key2=value2,...)或这样创建字典dict([(key1, value1), (key2, value2),...)])。这同样适用于此。您发布的作品称为文字
smido

2
@smido:谢谢。我还发现,您只能转换一个文字,即hashabledict({key_a: val_a, key_b: val_b, ...})
HelloGoodbye '18 -10-9

62

散列应该是不可变的-不强制执行此操作,但是请信任您不要在将dict作为键首次使用后对其进行突变,可以使用以下方法:

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

如果您确实需要更改您的命令并仍然想将它们用作键,那么复杂性会爆炸数百倍-并不是说无法完成,但是我将等到非常明确的指示后再进入那令人难以置信的烂摊子! -)


我当然不想在准备好命令后就改变它们。这将使其余的packrad算法崩溃。
SingleNegationElimination

然后,我建议的子类将起作用-注意使用in __key ;-) 绕过“位置”问题(编辑问题以指出问题之前sorted;-)。
亚历克斯·马丁里

namedtuple的位置相关行为令我惊讶。我一直在玩它,认为它可能仍然是解决问题的简便方法,但是这几乎打破了我的所有希望(并且需要回滚:()
SingleNegationElimination 2009年

假设我有一个dict,并且想将其转换为hashabledict。我该怎么办?
jononomo 2014年


32

使字典可用于您的目的所需要做的就是添加__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将直接访问这些值,并避免每次进行查找时都使用by 来在内存中形成新的许多键/值元组的许多内存分配器调用。


@RaymondHettinger如果我错了,请纠正我,但我认为dict它本身不会缓存其键的哈希值-尽管单个类(例如str)可以并且确实选择缓存其哈希值。至少当我dict使用自定义类实例作为键创建a时,它们的__hash__方法在每次访问操作(python 3.4)上都被调用。不管我是否正确,我都不知道如何hash(frozenset(self))重用预先计算的哈希值,除非它们被缓存在键本身内(在这种情况下,hash(frozenset(self.items())也要重用它们)。
最高

至于关于(键/值)元组创建的第二点,我认为.items()方法返回一个视图而不是元组列表,并且该视图的创建不涉及复制基础键和值。(再次使用Python 3.4。)也就是说,我确实看到了在大多数输入具有不同键的情况下仅对键进行哈希处理的优点-因为(1)哈希值非常昂贵,并且(2)要求值可哈希化是非常严格的
最多

6
这也有可能为两个不同的字典创建相同的哈希。考虑{'one': 1, 'two': 2}{'one': 2, 'two': 1}
AgDude

迈克·格雷厄姆(Mike Graham)在他的评论中指出,除了定义之外,出于其他任何原因派生dict __missing__都是一个坏主意。你怎么看?
Piotr Dobrogost '16

1
自python 2.2起,就已经很好地定义了dict的子类。有关Python标准库中的示例,请参见collections.OrderedDict和collections.Counter。另一条评论基于毫无根据的信念,即只有MutableMapping的子类得到了很好的定义。
Raymond Hettinger

23

给定的答案是可以的,但是可以通过使用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

性能优势取决于字典的内容,但是在我测试过的大多数情况下,使用进行哈希处理frozenset的速度至少要快2倍(主要是因为不需要排序)。


1
注意,不需要同时包含键和和值。该解决方案是快为: hash(frozenset(d))
Raymond Hettinger

10
@RaymondHettinger:hash(frozenset(d))使用相同的键但值不同,导致2个字典的哈希值相同!
Oben Sonne

4
那不是问题。__eq__的工作是区分不同值的dict。__hash__的工作仅仅是为了减少搜索空间。
Raymond Hettinger

5
这对于哈希和映射的理论概念是正确的,但对于将字典用作查找的缓存来说并不实用-将具有相似键但具有不同值的字典传递给内存缓存函数的情况并不罕见。在那种情况下,如果仅使用键来构建哈希,则缓存通常会变成列表而不是映射。
Oben Sonne

3
在带有相同键和不同值的字典的特殊情况下,最好存储基于的哈希值frozenset(d.itervalues())。在类型的字典有不同的键的情况下,frozenset(d)快,并规定上的按键hashability没有限制。最后,请记住dict .__ eq__方法将更快地检查相等的键/值对,以便任何东西都可以计算所有键/值对元组的哈希。使用键/值元组也是有问题的,因为它会丢弃所有键的存储哈希值(这就是为什么frozenset(d)这么快)。
Raymond Hettinger

11

合理干净,直接的实现是

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

为什么它如此合理,清洁和简单?即请解释的差异来其他的答案,例如必要性__iter____len__
2014年

1
@KarlRichter,我从未说过这是合理的,只是相当干净。;)
Mike Graham

@KarlRichter,我定义了__iter____len__因为我必须派生自定义collections.Mappingcollections.Mapping集合模块文档中很好地介绍了如何使用。因为他们是派生的,所以别人没有必要dictdict除了定义以外,出于任何其他原因进行派生__missing__都是一个坏主意。dict规范没有说明dict在这种情况下是如何工作的,实际上,这将最终导致大量非虚拟方法,这些方法通常不太有用,在这种情况下,将具有与行为无关的残留方法。
麦克·格雷厄姆

7

我继续回到这个话题...这是另一个变化。我对通过子类dict添加__hash__方法感到不安;字典的可变性几乎是无法逃脱的,相信它们不会改变似乎是一个很弱的主意。因此,我转而研究了基于本身不可变的内置类型构建映射。尽管这tuple是一个显而易见的选择,但访问其中的值却意味着排序和二等分。这不是问题,但似乎并没有充分利用其所构建类型的强大功能。

如果您卡住键,将值配对成,该frozenset怎么办?这将需要什么,它将如何工作?

第1部分,您需要一种编码“项目”的方式,以便冻结集将主要通过其键来对待它们。我将为此创建一个子类。

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

天哪!不幸的是,当您使用集合运算符时,元素是相等的但不是同一对象;最终返回值是不确定的,我们将不得不再旋转一些。

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

但是,以这种方式查找值很麻烦,而且更糟的是,它会创建许多中间集。那不会!我们将创建一个“假”键值对来解决它:

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

这样就减少了问题:

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

这就是所有深奥的魔力;其余的将它们全部包装成具有像dict这样的界面的东西。由于我们是从继承子类的frozenset,因此具有非常不同的接口,因此有很多方法。我们从那里得到了一些帮助collections.Mapping,但是大多数工作都覆盖了frozenset像dicts这样工作的版本的方法,而不是:

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

5

@Unknown接受的答案以及@AlexMartelli的答案可以很好地工作,但是仅在以下限制条件下:

  1. 字典的值必须是可哈希的。例如,hash(hashabledict({'a':[1,2]}))将引发TypeError
  2. 键必须支持比较操作。例如,hash(hashabledict({'a':'a', 1:1}))将引发TypeError
  3. 键上的比较运算符强加总顺序。例如,如果字典中的两个键是frozenset((1,2,3))frozenset((4,5,6)),则它们在两个方向上比较不相等。因此,使用此类关键字对字典的项目进行排序可能会导致任意顺序,因此将违反规则,即相等的对象必须具有相同的哈希值。

@ObenSonne更快的答案取消了约束2和3,但是仍然受约束1约束(值必须是可哈希的)。

@RaymondHettinger提供的更快但尚未解决的答案解除了所有3个约束,因为它不包括 .values()在哈希计算中。但是,仅在以下情况下其性能良好:

  1. 大多数需要散列的(非相等)字典不完全相同 .keys()

如果不满足此条件,则哈希函数将仍然有效,但可能会导致太多冲突。例如,在所有字典都是从网站模板生成的极端情况下(字段名称作为键,用户输入作为值),键将始终相同,并且哈希函数将为所有输入返回相同的值。结果,在检索项目时,依赖于此类哈希函数的哈希表将变得与列表一样慢(O(N)而不是O(1))。

我认为即使违反了我上面列出的所有4个约束,以下解决方案也可以很好地工作。它的另一个优点是,它不仅可以对字典进行哈希运算,还可以对任何容器进行哈希运算,即使它们具有嵌套的可变容器也是如此。

非常感谢您提供任何反馈,因为到目前为止,我仅对此进行了轻微测试。

# 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
    # e.g., 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
                # e.g. 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))

2

您可能还希望添加这两种方法,以使v2酸洗协议可与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),)

-2

如果您不将数字放入字典中,并且永远不会丢失包含字典的变量,则可以执行以下操作:

cache[id(rule)] = "whatever"

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

编辑:

哦,对不起,是的,在其他情况下,其他人说的会更好。我认为您也可以将字典序列化为字符串,例如

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

但是,如果您需要从按键中恢复字典,那么您就必须做些更丑陋的事情,例如

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

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


嗯,不;这不是我要寻找的东西:cache[id({'foo':'bar'})] = 'baz'; id({'foo':'bar'}) not in cache,能够动态创建键对于我首先要使用dict作为键很重要。
SingleNegationElimination 2012年

1
序列化命令可能没问题,您对序列化方法有建议吗?这就是我想要的。
SingleNegationElimination 2012年
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.