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

Overriding __eq__ and __hash__ without downgrading performace

提问人:Thomas 提问时间:5/13/2020 最后编辑:Thomas 更新时间:5/17/2020 访问量:738

问:

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

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

类结构:

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__

Python 性能 哈希 覆盖 相等性

评论

0赞 amain 5/13/2020
你为什么要用而不是来测试平等?运算符检查同一,即两个变量是否指向同一对象。使用并查看您的性能问题是否消失。is==is==
0赞 chepner 5/13/2020
object.__eq__之所以有效,是因为它除了简单地检查它们是否是同一对象之外,没有比较两个对象的基础。那是。__eq__ = lambda x, y: x is y
0赞 Thomas 5/13/2020
@amain我认为恰恰相反:比因为它只检查指针更快。无论如何,对于对象或不可变类型(如字符串),相等性最终会执行标识。在一个简单的测试中,相等需要两倍的时间。python -m timeit “'a' is 'a'” 10000000 次循环,三局两胜制:每循环 0.0415 微秒 python -m timeit “'a' == 'a'” 10000000 次循环,三局优制:每循环 0.0835 微秒is==
0赞 Thomas 5/13/2020
@chepner这正是我正在做的事情:测试身份。我的代码中只有一个额外的,我发现它的运行时间是原来的 10 倍。我不知何故觉得这是哈希部分的问题:当我ctrl + C时,执行总是在哈希内。这让我想到了另一个问题:如何避免每次调用时都计算哈希值?if__hash__
1赞 Thomas 5/14/2020
@amain当然,我做到了:它们的表现是一样的。如果你再读一遍你的第一条评论,你就会明白为什么我知道你声称比==is

答:

2赞 Aleksi Torhamo 5/17/2020 #1

就像评论中提到的,你有一个更大的问题:你的代码有错误。虽然可能稍微快一点,但它也做错了事情。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__()StructureFragmentcansmilescansmiles_unstarredAttributeError

现在,考虑一下代码的这一部分:

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

既然是 ,如果与 是同一个对象,那么 other 显然也是一个 ,所以 是不必要的。如果你看一下这两个问题,你就会意识到整个事情是错误的:你不需要检查 的类型,但你确实需要检查字符串检查的类型!因此:selfStructureselfotherStructureifis

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

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

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

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

如果你说 ,并调用该函数,而不是“只是指针比较”,你必须设置一个执行框架,为本地范围填充一个字典,对字典进行两次查找以根据名称获取值,将它们推送到堆栈中,从堆栈中弹出值,进行比较, 将结果推送到堆栈上,从堆栈中弹出结果,最后返回值 - 每个步骤都包括获取和解码字节码、跳转等。所以是的,这将比内置的要慢,后者实际上可能非常接近“只是一个指针比较”。def __eq__(self, other): return self is other__eq__()

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

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