在不降低性能的情况下覆盖 __eq__ 和 __hash__

Overriding __eq__ and __hash__ without downgrading performace

我有两个 classes 结构和片段代表化学结构。 如果我比较相同 class 的两个实例,比较应该正常进行,但如果我将结构与片段进行比较,比较应该基于唯一标识化学结构的字符串属性。

不幸的是,在我的尝试中 运行 时间急剧增加(集合和字典的操作)。

class结构:

def __eq__(self, other):
    if isinstance(other, Structure):
        return self is other
    else:
        return self.cansmiles is other.cansmiles_unstarred

def __hash__(self):
    return hash(self.cansmiles)

Fragment eqhash 是用相同的逻辑实现的。

"cansmiles" 字符串可能很长(通常为 25,最多 100),我试图将其切片但运气不佳。

问题是:内置的 hasheq 有什么魔力如此高效?有没有办法有效地覆盖它们?有什么建议吗?

更新:我可以避免每次调用 __hash__ 时都计算哈希值吗?

就像评论中提到的,你有一个更大的问题:你的代码有错误。虽然 is 可能稍微快一点,但它也会做错事。

虽然一些快速测试可能会让您相信它有效 - 由于 Python 优化了一些简单的案例 - 这些示例表明它通常不起作用:

def f():
    return 'not always identical!'

a = 'not always identical!'
b = f()

# Will print: True False -- that is, the strings are equal, but not the same string object
print(a == b, a is b)

x = 'a'
a = x*2
b = 'aa'

# Will print: True False -- that is, the strings are equal, but not the same string object
print(a == b, a is b)

至于您的 __eq__() 实现,如果您尝试比较 Structures 和 Fragments 以外的任何东西,它就会中断(因为其他对象可能没有 cansmiles/cansmiles_unstarred 属性,给出一个 AttributeError - 或者如果他们这样做,代码将做错事,因为你可能不想比较等于一个随机的未知对象)。 =46=]

现在,想想你的这部分代码:

if isinstance(other, Structure):
    return self is other

因为 self 是一个 Structure,如果 selfother 是同一个对象,其他显然也是一个 Structure - 所以if 有点 不必要。如果您查看这两个问题,您会发现整个事情都是错误的:您不需要检查 is 的类型,但您需要检查 is 的类型字符串检查!因此:

def __eq__(self, other):
    if isinstance(other, Fragment):
        return self.cansmiles == other.cansmiles_unstarred
    else:
        return self is other

现在,至于此更改将如何影响字典查找的速度:不会。 Python 通过首先检查 is 来优化字典查找,然后再回到 ==。因此,如果您使用与保存数据时相同的类型进行查找,由于同一类型内的相等性是基于身份的,因此 __eq__() 唯一会被调用的时间是如果您有哈希冲突(罕见)。

如果您使用其他类型进行查找,则对象将不相同,因此它会退回到 == 并调用 __eq__() - 但从is== 仍然不会影响速度:== 可能比 is 稍慢,但是函数调用要慢一个数量级,所以所花费的时间isinstance() 将使任一比较所花费的时间相形见绌。

那么为什么 内置的 __eq__()__hash__() 这么快?那么,为什么 sum(l)for v in l: t += v 快?因为 运行 Python 字节码比 运行 本机代码慢得多。在评论中,您说 is 仅检查指针 - 但事实并非如此,对吗?您必须从字节码中获取下一个操作码及其参数,select 正确的处理程序,跳转到处理程序等等。

如果你说 def __eq__(self, other): return self is other 并调用那个函数,而不是 "just a pointer comparison",你必须设置一个执行框架,为本地范围填充一个字典,对字典根据名称获取值,将它们压入堆栈,从堆栈中弹出值,进行比较,将结果压入堆栈,从堆栈中弹出结果,最后 return 值- 每一步都包括获取和解码字节码、跳转等等。所以是的,这将比内置 __eq__() 慢,后者实际上可能非常接近 "just a pointer comparison".

至于避免重新计算散列:您已经这样做了 - 您正在散列字符串,并且字符串缓存了它们的散列,因此它们不会被重新计算。内置的 __hash__() 是基于对象的 id,所以它应该是常数时间的,所以它显然比计算字符串的哈希更快,后者应该是线性时间的。但即使字符串哈希被重新计算,那也是在本机代码中完成的,所以速度差异会很小——而且与你从本机代码切换到 运行 python 字节码的事实相形见绌当你定义一个自定义的时候。

你当然可以缓存 return 由 hash() 自己编辑的值 - 你需要添加一个 if 来检查哈希是否已经计算,但是由于调用函数相对慢,即使添加了代码,结果仍可能 稍微 更快。但是您将添加代码并降低可读性,以获得非常小的速度提升。如果你想要速度,为什么不直接使用字符串作为键呢?或者,如果速度真的很重要,您可能想看看 PyPy(甚至 Numba)。