__eq__() 多次调用,而不是在嵌套数据结构中调用一次

__eq__() called multiple times instead of once in nested data structure

提问人:Feuermurmel 提问时间:4/9/2021 最后编辑:David ZFeuermurmel 更新时间:5/24/2023 访问量:268

问:

每年一两次,我会遇到以下问题:我有某种类型的比较操作可能很昂贵(例如,值很大以保存在内存中并且需要从磁盘加载,或者等式很难计算,因为单个值可能有很多表示,想想化学公式)。此类型是嵌套数据结构(例如嵌套列表或元组或某些树)的一部分。有时我注意到,对于单个比较的相同值,我的类型的比较运算符(等)被多次调用。__lt__

我将尝试通过以下示例来说明问题:

class X:
    comparisons = 0

    def __init__(self, value):
        self.value = value

    def __lt__(self, other):
        return self.value < other.value

    def __gt__(self, other):
        return self.value > other.value

    def __eq__(self, other):
        X.comparisons += 1
        return self.value == other.value

def nest_a_hundred_times(value):
    for i in range(100): value = [value]
    return value

print(nest_a_hundred_times(X(1)) < nest_a_hundred_times(X(0)))
print(X.comparisons)

在此示例中,我的类型具有昂贵的比较操作,我只是计算调用的次数,但其他操作也可能很昂贵。该类型的两个不相等值被创建并嵌套在单元素列表中很多次。X__eq__()

运行示例将打印 、 。所以被叫了100次。False100__eq__()

我知道为什么会发生这种情况:列表对象的内置比较函数首先比较单个列表元素的相等性,以找出两个列表在哪个索引处不同,然后再比较这些元素进行排序。我认为,当仅使用六个比较运算符(、、、、)作为定义排序的类型之间的接口时,实际上不可能避免这个问题。作为替代方法的一个例子,Haskell 有一个 Ord 类,它定义了一个函数来比较两个值。这允许通过在每个节点上仅调用一次来比较嵌套数据结构。==!=<<=>>=orderingordering

我的问题是:如何在 Python 中避免这个问题?与我的信念相反,是否有可能单独使用 Python 定义的比较运算符来避免这个问题?(我试图避免某种结果缓存,因为这不是性能问题,而是算法问题)。或者我是否需要构建自己的数据结构(列表、元组)并在其上实现函数?ordering

Python 数据结构 比较

评论

0赞 user2357112 4/9/2021
问题在于,每个列表的调用都需要同时执行调用和对下一个嵌套列表的调用。这是与从 3 向比较切换到富比较相关的性能回归。list.__lt__==<
1赞 user2357112 4/9/2021
(您可以在此处查看代码。
0赞 Feuermurmel 4/9/2021
@user2357112supportsMonica 谢谢。不过,我知道这一点。
0赞 David Z 4/12/2021
不过,在这里的某个地方提到这个事实是非常好的(或者至少链接到一个提供解释的地方),因为对于那些不明白为什么一直被称为“为什么”的人来说,这个问题可能会更加令人困惑。我可以建议编辑问题的解释或链接(或两者兼而有之)吗?__eq__
1赞 Feuermurmel 4/12/2021
@DavidZ 谢谢你这么好心。:)我明白你的意思。我本人希望每个人都能不时地使用“向原作者提出此编辑”功能。就像 GitHub 上的拉取请求一样。

答:

0赞 Modularizer 10/27/2022 #1

从你提出问题的方式来看,我假设:

  • 如果可能,您不希望覆盖。(我也不想,这是一个非常危险的想法)。list.__eq__
  • 如果需要,您可以覆盖 dunder () 方法。(据我所知,这是需要的)。__X

正如你所暗示的,因为你正在尝试解决在内置类型上实现内置操作的问题,所以我认为任何解决方案都不会特别干净(但嘿,也许其他答案会让我感到惊讶)。

我发现的一件有趣的事情是,如果你覆盖返回,它只会被调用一次。X.__eq__True

class X:
    ...
    def __eq__(self, other):
        X.comparisons += 1
        return True

现在显然这可能会产生一些问题,因为它会使 .但是,它会使和工作并且效率更高,所以如果你是积极的,你永远不需要使用 ==,我认为这可能是一种方式。X(1)==X(0)nest_a_hundred_times(X(1)) == nest_a_hundred_times(X(0))<>

除此之外,我能想到的只是一个公认的混乱的黑客攻击,试图检测是被“>”还是由......__eq__<==

import inspect

class X:
    ...
    def __eq__(self, other):
        X.comparisons += 1
        f = inspect.currentframe().f_back
        fi = inspect.getframeinfo(f)
        line_called_from = fi[-2][0]
        called_from_lt = ('<' in line_called_from  or '>' in line_called_from) and '==' not in line_called_from and 'eq(' not in line_called_from
        if called_from_lt:
            return True
        return self.value == other.value
0赞 Sam 5/24/2023 #2

Your best option given the way list comparisons work might be to either:

a) If possible, when first needed, summarize and cache each value with a unique hash (e.g., a sortable string) that can be compared as a proxy for the heavyweight data you would otherwise compare; or

b) maintain a cache/memoization of recent comparison results based on object identities, and if a hit is found, just return the same result.