__eq__ 中包含着隐藏关系

Python 2018-10-11

相不相等我说了算,本文讨论的都是在python3的环境下

起步

__eq__ 是用来重载操作符 == 的;相对的,__ne__ 用来重载 != 。这两个魔术方法存在一个容易忽略的隐藏关系,就是当类中定义了 __eq__ 而没有定义 __ne__ 的时候,对于 != 操作会取 __ne__() 的反向结果。

但反过来就不成立了,如果定义 __ne__ 而没有定义 __eq__ ,对于 == 操作则会进行默认的行为。也就是说 x != y 不一定就是 x == y 的相反结果。它们之间并不存在这样的隐藏调用的关系。

所以,绝大多数情况下,重写 __eq__ 方法就够了。

其他比较操作符:

object.__lt__(self, other)
object.__le__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)

也都不存在这个关系。没定义的操作符会抛出 NotImplemented 异常。

引发的思考

在 python 中所有类都继承自 object ,而 object.__ne__ 是存在的:

>>> class A:
...     def __eq__(self, other):
...         return True
...
>>> id(object.__ne__)
2024343946008
>>> id(A.__ne__)
2024343946008
>>> a = object()
>>> b = object()
>>> a == b
False
>>> a != b
True
>>> a = A()
>>> b = A()
>>> a == b
True
>>> a != b
False
>>>

这里的问题是,A.__ne__ 从基类继承过来,它明明就存在的啊,为什么这边没有去调用而是通过 A.__eq__ 去判断呢,很奇怪是不是?

这要探究就得深入CPython中去了,执行 __ne__ 是在 typeobject.cobject_richcompare 中:

static PyObject *
object_richcompare(PyObject *self, PyObject *other, int op)
{
    PyObject *res;

    switch (op) {

    case Py_EQ:
        res = (self == other) ? Py_True : Py_NotImplemented;
        Py_INCREF(res);
        break;

    case Py_NE:
        /* By default, __ne__() delegates to __eq__() and inverts the result,
           unless the latter returns NotImplemented. */
        // 上面的注释是,__ne__ 委托给 __eq__ 来执行,对结果取反
        if (self->ob_type->tp_richcompare == NULL) {
            res = Py_NotImplemented;
            Py_INCREF(res);
            break;
        }
        res = (*self->ob_type->tp_richcompare)(self, other, Py_EQ);
        if (res != NULL && res != Py_NotImplemented) {
            int ok = PyObject_IsTrue(res);
            Py_DECREF(res);
            if (ok < 0)
                res = NULL;
            else {
                if (ok)
                    res = Py_False;
                else
                    res = Py_True;
                Py_INCREF(res);
            }
        }
        break;
    }

    return res;
}

这里注释写得很清楚了,重点在于 res = (*self->ob_type->tp_richcompare)(self, other, Py_EQ); 中执行了 PyObject_RichCompare ,这是一种性能比较低的比较方式,它实际上使用默认的逻辑 __eq__ ,这部分在 object.cdo_richcompare 函数中。

static PyObject *
do_richcompare(PyObject *v, PyObject *w, int op)
{
    richcmpfunc f;
    PyObject *res;
    int checked_reverse_op = 0;

    if (v->ob_type != w->ob_type &&
        PyType_IsSubtype(w->ob_type, v->ob_type) &&
        (f = w->ob_type->tp_richcompare) != NULL) {
        checked_reverse_op = 1;
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = v->ob_type->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if (!checked_reverse_op && (f = w->ob_type->tp_richcompare) != NULL) {
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    /* If neither object implements it, provide a sensible default
       for == and !=, but raise an exception for ordering. */
    switch (op) {
    case Py_EQ:
        res = (v == w) ? Py_True : Py_False;
        break;
    case Py_NE:
        res = (v != w) ? Py_True : Py_False;
        break;
    default:
        PyErr_Format(PyExc_TypeError,
                     "'%s' not supported between instances of '%.100s' and '%.100s'",
                     opstrings[op],
                     v->ob_type->tp_name,
                     w->ob_type->tp_name);
        return NULL;
    }
    Py_INCREF(res);
    return res;
}

不管在 PyObject_RichCompare 还是在 do_richcompare 我都看到了 if (res != Py_NotImplemented) 这样的语句。难道我们可以在比较操作中返回 NotImplemented ?我不禁要试一下:

class A:
    def __eq__(self, other):
        return NotImplemented
    def __ne__(self, other):
        return NotImplemented

a = A()
b = A()
print(a == b) # True
print(a != b) # True
print(a != a) # False

显然这里的 __ne__ 被实现,不会去调用 __eq__ 了,而我们返回了 NotImplemented ,也就是告诉解释器这些方法不存在,这就让 do_richcompare 中的 3 个 if (res != Py_NotImplemented) 都不成立,乖乖走后续的 switch 块,这部分就直接通过指针是否相等来判断了。

性能比较

通过 PyObject_RichCompare 来比较效率会比较低,甚至有的需要处理 NotImplemented 的情况。我们对这几种情况做个对比:

class Default:
    pass

class NeOnly:
    def __ne__(self, other):
        return not self == other

class RNotImplemented:
    def __ne__(self, other):
        return NotImplemented

def c_level():
    cl = Default()
    return lambda: cl != cl

def high_level_python():
    hlp = NeOnly()
    return lambda: hlp != hlp

def low_level_python():
    llp = RNotImplemented()
    return lambda: llp != llp

import timeit
print(min(timeit.repeat(c_level())))          # 0.10529670296476416
print(min(timeit.repeat(high_level_python())))# 0.31792451405355615
print(min(timeit.repeat(low_level_python()))) # 0.3742690036398786

数字很有说服力。

总结

对于比较操作符,只有一个关联的关系:当未定义 __ne__ 时,则会委托给 __eq__ 并将结果取反

参考

python-should-i-implement-ne-operator-based-on-eq

https://bugs.python.org/issue4395


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!