首先,如果您非常确定 O(N) 散列在这里是合理且必要的,并且您只是想使用比 hash(str(x)) 更快的算法来加快速度,试试这个:
def hash_seq(iterable):
result = hash(type(iterable))
for element in iterable:
result ^= hash(element)
return result
当然,这不适用于可能很深的序列,但有一个明显的解决方法:
def hash_seq(iterable):
result = hash(type(iterable))
for element in iterable:
try:
result ^= hash(element)
except TypeError:
result ^= hash_seq(element)
return result
我不认为这是一个足够好的哈希算法,因为它会为同一个列表的不同排列返回相同的值。但我很确定没有足够好的哈希算法会更快。至少如果它是用 C 或 Cython 编写的,如果这是你的方向,你最终可能会想要这样做。
另外,值得注意的是,这在str(或marshal)不正确的许多情况下是正确的,例如,如果您的list 可能有一些可变元素,其repr 涉及其@987654334 @ 而不是它的值。但是,它仍然不是在所有情况下都是正确的。特别是,它假设“迭代相同的元素”意味着任何可迭代类型的“相等”,这显然不能保证是真的。误报不是什么大问题,但误报是(例如,两个 dicts 具有相同的键但不同的值可能会虚假地比较相等并共享一个备忘录)。
此外,它不使用额外的空间,而不是使用相当大的乘数的 O(N)。
无论如何,值得先尝试一下,然后再决定是否值得分析它是否值得分析以确保足够好并进行微调以进行微优化。
这是一个简单的 Cython 版本的浅层实现:
def test_cy_xor(iterable):
cdef int result = hash(type(iterable))
cdef int h
for element in iterable:
h = hash(element)
result ^= h
return result
通过快速测试,纯 Python 实现非常慢(正如您所料,所有 Python 循环,与 str 和 marshal 中的 C 循环相比),但 Cython 版本很容易获胜:
test_str( 3): 0.015475
test_marshal( 3): 0.008852
test_xor( 3): 0.016770
test_cy_xor( 3): 0.004613
test_str(10000): 8.633486
test_marshal(10000): 2.735319
test_xor(10000): 24.895457
test_cy_xor(10000): 0.716340
只是在 Cython 中迭代序列并且什么都不做(实际上只是对 PyIter_Next 的 N 次调用和一些引用计数,所以你不会在本机 C 中做得更好)是 @ 的 70% 987654341@。您可能可以通过要求实际序列而不是可迭代来使其更快,甚至通过要求list 来加快速度,尽管无论哪种方式都可能需要编写显式 C 而不是 Cython 才能获得好处。
无论如何,我们如何解决订购问题?显而易见的 Python 解决方案是散列 (i, element) 而不是 element,但所有这些元组操作都会将 Cython 版本的速度降低 12 倍。标准解决方案是在每个异或之间乘以某个数字。但是,当您使用它时,值得尝试让这些值很好地分布在短序列、小int 元素和其他非常常见的边缘情况下。选择正确的数字很棘手,所以……我只是从tuple 借了所有东西。这是完整的测试。
_hashtest.pyx:
cdef _test_xor(seq):
cdef long result = 0x345678
cdef long mult = 1000003
cdef long h
cdef long l = 0
try:
l = len(seq)
except TypeError:
# NOTE: This probably means very short non-len-able sequences
# will not be spread as well as they should, but I'm not
# sure what else to do.
l = 100
for element in seq:
try:
h = hash(element)
except TypeError:
h = _test_xor(element)
result ^= h
result *= mult
mult += 82520 + l + l
result += 97531
return result
def test_xor(seq):
return _test_xor(seq) ^ hash(type(seq))
hashtest.py:
import marshal
import random
import timeit
import pyximport
pyximport.install()
import _hashtest
def test_str(seq):
return hash(str(seq))
def test_marshal(seq):
return hash(marshal.dumps(seq))
def test_cy_xor(seq):
return _hashtest.test_xor(seq)
# This one is so slow that I don't bother to test it...
def test_xor(seq):
result = hash(type(seq))
for i, element in enumerate(seq):
try:
result ^= hash((i, element))
except TypeError:
result ^= hash(i, hash_seq(element))
return result
smalltest = [1,2,3]
bigtest = [random.randint(10000, 20000) for _ in range(10000)]
def run():
for seq in smalltest, bigtest:
for f in test_str, test_marshal, test_cy_xor:
print('%16s(%5d): %9f' % (f.func_name, len(seq),
timeit.timeit(lambda: f(seq), number=10000)))
if __name__ == '__main__':
run()
输出:
test_str( 3): 0.014489
test_marshal( 3): 0.008746
test_cy_xor( 3): 0.004686
test_str(10000): 8.563252
test_marshal(10000): 2.744564
test_cy_xor(10000): 0.904398
以下是一些可以加快速度的潜在方法:
- 如果您有很多深度序列,不要在
hash 周围使用 try,而是调用 PyObject_Hash 并检查 -1。
- 如果你知道你有一个序列(或者,甚至更好,特别是一个
list),而不仅仅是一个可迭代的,PySequence_ITEM(或PyList_GET_ITEM)可能会隐含地比PyIter_Next更快上面用过。
在任何一种情况下,一旦您开始调用 C API 调用,通常更容易放弃 Cython 并用 C 编写函数。(您仍然可以使用 Cython 围绕该 C 函数编写一个简单的包装器,而不是手动编码扩展模块。)此时,只需直接借用tuplehash 代码,而不是重新实现相同的算法。
如果您首先要寻找避免O(N) 的方法,那是不可能的。如果你看看tuple.__hash__、frozenset.__hash__ 和ImmutableSet.__hash__ 是如何工作的(顺便说一句,最后一个是纯Python 并且非常易读),它们都采用O(N)。但是,它们也都缓存了哈希值。因此,如果您经常对 same tuple(而不是不相同但相等的)进行散列处理,它会接近恒定时间。 (它是O(N/M),其中M 是您与每个tuple 通话的次数。)
如果您可以假设您的 list 对象在调用之间永远不会发生变化,那么您显然可以做同样的事情,例如,将 dict 映射为 id 到 hash 作为外部缓存。但总的来说,这显然不是一个合理的假设。 (如果您的 list 对象永远不会发生变异,那么只需切换到 tuple 对象而不用担心所有这些复杂性会更容易。)
但是您可以将 list 对象包装在一个添加缓存哈希值成员(或槽)的子类中,并在收到变异调用时使缓存无效(append、__setitem__、__delitem__ , ETC。)。然后你的hash_seq 可以检查。
最终结果与tuples 的正确性和性能相同:摊销O(N/M),除了tuple M 是您使用每个相同的tuple 调用的次数,而对于@ 987654383@ 是您调用每个相同的 list 的次数,而不会在两者之间发生变异。