在不降低性能的情况下重写uuu eq uuuuuu和uuu hash uuuuuuuu

2024-10-04 01:28:48 发布

您现在位置:Python中文网/ 问答频道 /正文

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

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

班级结构:

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)

片段eqhash使用相同的逻辑实现

“cansmiles”字符串可能相当长(通常为25,最多为100),我试图在没有运气的情况下将其切碎

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

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


Tags: 实例字符串selfreturn属性isdef代表
1条回答
网友
1楼 · 发布于 2024-10-04 01:28:48

就像在评论中提到的,你有一个更大的问题:你的代码有bug。虽然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__()实现,如果您尝试比较StructureFragment之外的任何东西,它就会崩溃(因为其他对象可能没有cansmiles/cansmiles_unstarred属性,给出一个AttributeError-或者如果它们有,代码会做错误的事情,因为您可能不想与随机未知对象进行比较)

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

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

因为self是一个Structure,如果selfother是同一个对象,那么另一个显然也是一个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()所花费的时间将使两种比较所花费的时间相形见绌

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

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

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

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

相关问题 更多 >