提问人:Thomas 提问时间:5/13/2020 最后编辑:Thomas 更新时间:5/17/2020 访问量:738
在不降低性能的情况下覆盖__eq__和__hash__
Overriding __eq__ and __hash__ without downgrading performace
问:
我有两个类,结构和片段,分别代表化学结构。 如果我比较同一类的两个实例,则比较应该表现正常,但是如果我将结构与片段进行比较,则比较应基于唯一标识化学结构的字符串属性。
不幸的是,在我的尝试中,运行时间急剧增加(使用集合和字典的操作)。
类结构:
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)
片段均衡器和哈希使用相同的逻辑实现。
“cansmiles”字符串可能很长(通常为 25 个,最多 100 个),我试图切成薄片,但没有运气。
问题是:内置哈希和均衡器有什么神奇之处才能如此高效?有没有办法有效地覆盖它们?有什么建议吗?
更新:我可以避免每次调用时都计算哈希值吗?__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)
至于你的实现,如果你试图比较 s 和 s 以外的任何内容,它就会中断(因为其他对象可能没有 / 属性,给出一个 - 或者如果它们有,代码会做错误的事情,因为你可能不想比较等于一个随机的未知对象)。__eq__()
Structure
Fragment
cansmiles
cansmiles_unstarred
AttributeError
现在,考虑一下代码的这一部分:
if isinstance(other, Structure):
return self is other
既然是 ,如果与 是同一个对象,那么 other 显然也是一个 ,所以 是不必要的。如果你看一下这两个问题,你就会意识到整个事情是错误的:你不需要检查 的类型,但你确实需要检查字符串检查的类型!因此:self
Structure
self
other
Structure
if
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()
那么,为什么内置的要快得多呢?那么,为什么比 ?因为运行 Python 字节码比运行本机代码慢得多。在评论中,您说只检查指针 - 但这不是真的,是吗?您必须从字节码中获取下一个操作码及其参数,选择正确的处理程序,跳转到处理程序等。__eq__()
__hash__()
sum(l)
for v in l: t += v
is
如果你说 ,并调用该函数,而不是“只是指针比较”,你必须设置一个执行框架,为本地范围填充一个字典,对字典进行两次查找以根据名称获取值,将它们推送到堆栈中,从堆栈中弹出值,进行比较, 将结果推送到堆栈上,从堆栈中弹出结果,最后返回值 - 每个步骤都包括获取和解码字节码、跳转等。所以是的,这将比内置的要慢,后者实际上可能非常接近“只是一个指针比较”。def __eq__(self, other): return self is other
__eq__()
至于避免重新计算哈希值:你已经这样做了 - 你正在对字符串进行哈希处理,而字符串缓存它们的哈希值,所以它们不会被重新计算。内置的 ID 基于对象的 id,因此它应该是恒定时间,因此它显然比计算字符串的哈希值更快,字符串应该是线性时间。但是,即使重新计算字符串哈希,也是在本机代码中完成的,因此速度差异很小 - 并且与定义自定义代码时从本机代码切换到运行 python 字节码的事实相形见绌。__hash__()
你当然可以缓存自己返回的值 - 你需要添加一个 if 来检查是否已经计算出哈希值,但由于调用函数的速度相对较慢,即使添加了代码,结果可能仍然会稍微快一些。但是你会添加代码,并降低可读性,以获得非常小的速度提升。如果你想要速度,为什么不直接使用字符串作为键呢?或者,如果速度真的很重要,你可能想看看 PyPy(甚至 Numba)。hash()
评论
is
==
is
==
object.__eq__
之所以有效,是因为它除了简单地检查它们是否是同一对象之外,没有比较两个对象的基础。那是。__eq__ = lambda x, y: x is y
is
==
if
__hash__
==
is