在Python中散列不可变字典

Hashing an immutable dictionary in Python

短版本:对于实现为无序项字典的多集,什么是最好的哈希算法?

我试图散列一个不可变的多集(在其他语言中是一个包或多集:就像一个数学集,除了它可以容纳多个元素之外)作为字典实现。我已经创建了标准库类collections.Counter的一个子类,类似于这里的建议:python hashable dicts,它推荐了这样的哈希函数:

1
2
3
4
class FrozenCounter(collections.Counter):
    # ...
    def __hash__(self):
        return hash(tuple(sorted(self.items())))

创建完整的项目元组需要占用大量的内存(例如,相对于使用生成器),散列将发生在应用程序中一个非常占用内存的部分。更重要的是,我的字典键(多集元素)可能无法订购。

我正在考虑使用这个算法:

1
2
def __hash__(self):
    return functools.reduce(lambda a, b: a ^ b, self.items(), 0)

我认为使用位异或意味着顺序对于散列值来说并不重要,不像对元组进行散列那样?我想我可以在我的数据元组的无序流上半实现python元组哈希算法。请参阅https://github.com/jonashaag/cpython/blob/master/include/tupleobject.h(在页面中搜索"hash"一词),但我几乎不知道足够的C来读它。

思想?建议?谢谢。


(如果你想知道我为什么要搞混一个多重集:我问题的输入数据是一组多重集,在每个多重集内,每个多重集都必须是唯一的。我在工作的最后期限,我不是一个经验丰富的程序员,所以我想避免发明新的算法在可能的情况下。要确保我有一堆独特的东西,最像是Python的方法是把它们放在一个set()中,但这些东西必须是可以散列的。)

我从评论中收集到的信息

@marcin和@senderle给出了几乎相同的答案:使用hash(frozenset(self.items()))。这是有意义的,因为items()的"视图"设置为类似的。@Marcin是第一个,但我给@senderle打了个勾,因为我对不同解决方案的大O运行时间做了很好的研究。@Marcin还提醒我包括一个__eq__方法——但是从dict继承的方法会很好地工作。这就是我实现一切的方式——欢迎基于此代码的进一步评论和建议:

1
2
3
4
5
6
7
8
9
10
11
12
class FrozenCounter(collections.Counter):
    # Edit: A previous version of this code included a __slots__ definition.
    # But, from the Python documentation:"When inheriting from a class without
    # __slots__, the __dict__ attribute of that class will always be accessible,
    # so a __slots__ definition in the subclass is meaningless."
    # http://docs.python.org/py3k/reference/datamodel.html#notes-on-using-slots
    # ...
    def __hash__(self):
       "Implements hash(self) -> int"
        if not hasattr(self, '_hash'):
            self._hash = hash(frozenset(self.items()))
        return self._hash


因为字典是不可变的,所以可以在创建字典时创建哈希,并直接返回它。我的建议是从items创建一个frozenset(在3+中;iteritems在2.7中),散列它,并存储散列。

提供一个明确的例子:

1
2
3
4
5
6
>>>> frozenset(Counter([1, 1, 1, 2, 3, 3, 4]).iteritems())
frozenset([(3, 2), (1, 3), (4, 1), (2, 1)])
>>>> hash(frozenset(Counter([1, 1, 1, 2, 3, 3, 4]).iteritems()))
-3071743570178645657
>>>> hash(frozenset(Counter([1, 1, 1, 2, 3, 4]).iteritems()))
-6559486438209652990

为了解释为什么我更喜欢frozenset而不是排序项的元组:frozenset不需要对项进行排序(因为它们是由内存中的散列稳定排序的),因此初始散列应该在O(n)时间而不是O(n log n)时间内完成。这可以从frozenset_hashset_next实现中看到。


你考虑过hash(sorted(hash(x) for x in self.items()))吗?这样,您只需要对整数进行排序,而不必构建列表。

您也可以将元素散列在一起XOR,但坦率地说,我不知道这有多有效(您会有很多冲突吗?).说到碰撞,你不需要实现__eq__方法吗?

或者,与我这里的回答类似,hash(frozenset(self.items()))