【问题标题】:Inconsistent object comparison behaviour when inheriting from dict从 dict 继承时对象比较行为不一致
【发布时间】:2015-03-05 20:46:33
【问题描述】:

这个问题是由一个失败的测试引起的,该测试拒绝在本地失败,并且只会在我们的 CI 服务器上失败。

结果是无意中进行了一些相当狡猾的对象比较。

我现在很好奇为什么同一 Python 版本 (2.7.9) 的两个安装之间的行为如此不同。

这个测试用例可能会进一步简化,但这就是我得到的:

import operator


class Thing(dict):
    def __int__(self, number):
        return self['number']

    def __gt__(self, other):
        return self['number'] > other

thing = Thing({'number': 2})

for o in [
        operator.lt,
        operator.le,
        operator.eq,
        operator.ne,
        operator.ge,
        operator.gt]:
    print o
    print o(0.01, thing)
    print o(thing, 0.01)

而在本地运行的结果是:

<built-in function lt>
True
False
<built-in function le>
True
False
<built-in function eq>
False
False
<built-in function ne>
True
True
<built-in function ge>
False
True
<built-in function gt>
False
True

但在 Travis CI 服务器上是:

<built-in function lt>
True
True
<built-in function le>
False
True
<built-in function eq>
False
False
<built-in function ne>
True
True
<built-in function ge>
True
False
<built-in function gt>
True
True

Python 回退到什么样的比较行为,为什么它会在相同版本的两个安装中表现出如此不同的行为?

我最初的想法是某种基于id 的比较,但从查看id 的值来看,它们与比较结果完全不相关。

更新:

这种不同的行为只发生在类继承自dict 时。当它从 object 继承时,比较在两种安装上的行为相同,并给出与上述本地结果相同的结果。

更新 2:

我刚刚发现只使用 __int____gt__ 方法可以进一步简化测试用例,但是如果我删除其中任何一个方法,那么奇怪的行为就会消失。

【问题讨论】:

  • 一个有趣的问题!如果您使用仅继承自 object 而不是 dict 的类,您能否重现该行为?我还没有弄清楚发生了什么,但要注意的一件事是dict 定义了一些您的类没有的比较操作(实际上它定义了所有普通的不等式比较)。
  • 在我的机器上尝试这个 le,ne 和 ge 不调用类中定义的函数。这对我来说似乎很有趣,因为您的服务器和本地计算机上的答案不同,但据我了解,如果只使用内置函数,不同的答案......很奇怪?
  • 我刚才也在想同样的事情,发现只有从dict继承时才会发生这种行为。我已经简化了上面的测试用例。从object 继承时,它们在两种安装中的行为相同。
  • functools.total_ordering --> "填充缺失排序方法的类装饰器"...但是dict中没有缺失排序方法。
  • @tdelaney,这很好!但是什么可以解释两个安装之间的行为差​​异呢?

标签: python comparison-operators


【解决方案1】:

如 cmets 中所述,dict 已经定义了所有比较运算符。 documented 行为是:

不同类型的对象,除了不同的数值类型和不同的字符串类型,从不比较相等;此类对象的顺序一致但随意

换句话说,字典被专门定义为允许与其他类型的比较,但这种比较的结果是不确定的。 (这在 Python 3 中已更改,因此不再允许此类类型间比较。)

当您只覆盖您的类型的一些比较运算符时,您会使事情变得更加复杂。由于您的类型定义了__gt__ 而不是__lt__thing &gt; 0.01 将使用您的自定义__gt__,但thing &lt; 0.01 将使用默认(未定义)比较行为。因此,您得到的类型有时使用确定性规则,有时会给出未定义的行为,具体取决于您使用的比较运算符。我不知道为什么您会看到您所看到的结果的精确模式,但底线是您的类依赖于未定义的行为,因此您不能期望使用这种类型的比较有任何一致性。 Python 的两种实现可能在某些神秘的实现级别上做不同的事情,从而产生不同的未定义行为。未定义行为的关键在于您不应该知道它是如何工作的(或者您可能开始依赖它)。

顺便说一句,total_ordering 这里是一个无操作,如果你删除它,行为应该是一样的。 total_ordering 仅添加尚未定义的比较运算符,但 dict 已经定义了所有比较运算符,因此 total_ordering 不会做任何事情。如果您想在已经定义了自己的比较行为(如 dict)的类型的子类上建立自己的排序关系,那么您需要手动覆盖每个单独的比较运算符。

【讨论】:

  • 我很难理解的一点行为是,当我从类中删除 __int__ 方法时,它不再表现出这种行为,但从未调用过 __int__ 方法当进行比较时..
  • 啊,如果对象有 __int__ 方法,PyNumber_Check 是否返回 True?如果它们都通过了PyNumber_Check,那么它会退回到最后的比较,即内存指针?
  • @Acorn:这听起来很合理。我同意这令人惊讶,但它仍然是有效的未定义行为:-)。
  • 是的,经过进一步调查,最后的比较是比较对象类型的内存位置。在本地和 CI 服务器上比较 id(type(thing))id(type(0.02)) 后,我发现 Thingid 在本地总是更高,但在 CI 服务器上总是更低!谜团解开了:)
【解决方案2】:

经过进一步调查,根据@BrenBarn 的精彩回答,我找到了奇怪行为的根源。

“未定义”比较的最后一步是比较对象类型的内存位置。在本地和CI服务器上比较id(type(thing))id(type(0.02))后,我发现Thing的id在本地总是较高,在CI服务器上总是较低!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-11
    • 1970-01-01
    相关资源
    最近更新 更多