简体   繁体   English

Python运算符重载到底如何工作?

[英]How exactly does Python operator overloading work?

I tried the following experiment: 我尝试了以下实验:

>>> class A(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return self.name
...     def __eq__(self, other):
...         print('{}.__eq__({})'.format(self, other))
...         return NotImplemented
... 
>>> a1 = A('a1')
>>> a2 = A('a2')
>>> a1 == a2
a1.__eq__(a2)
a2.__eq__(a1)
a1.__eq__(a2)
a2.__eq__(a1)
a2.__eq__(a1)
a1.__eq__(a2)
False

What the heck is going on here? 这里到底发生了什么?

More generally, is there official documentation of the exact flow that occurs when evaluating an operator? 更一般而言,是否存在评估操作员时发生的确切流程的官方文档? The data model documentation alludes to some kind of fallback behavior but does not precisely describe what it is. 数据模型文档暗示了某种后备行为,但没有精确描述它是什么。 There are several possibilities that complicate the situation: 有几种使情况复杂化的可能性:

  • a and b may be of the same or different types ab可以是相同或不同的类型
  • a and b may or may not not define an __eq__ method ab可能会或可能不会定义__eq__方法
  • a.__eq__(b) or b.__eq__(a) may return NotImplemented , or another value. a.__eq__(b)b.__eq__(a)可能返回NotImplemented或其他值。

Some kind of flow diagram would be helpful. 某种流程图会有所帮助。


Edit: Before marking this question as a duplicate, please make sure the supposed duplicate answers the following: 编辑:在将此问题标记为重复之前,请确保假定的重复回答以下问题:

  • Why does __eq__ get called 6 times in the given pattern? 为什么在给定的模式下__eq__被调用6次?
  • Where is the behavior fully documented? 该行为在何处得到充分记录?
  • Can I get a flowchart? 我可以得到流程图吗?

The behaviour is python-2.x only and it's part of how rich comparisons work internally (at least CPython) but only if both are new-style classes and both arguments have the same type! 该行为仅是python-2.x,并且是内部丰富的比较如何工作的一部分(至少是CPython),但前提是两者都是新样式类且两个参数都具有相同的类型!

The source C-code reads (I highlighted the parts where the comparisons are done and/or skipped): 源C代码读取(我突出显示了完成和/或跳过比较的部分):

PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    PyObject *res;

    assert(Py_LT <= op && op <= Py_GE);
    if (Py_EnterRecursiveCall(" in cmp"))
        return NULL;

    /* If the types are equal, and not old-style instances, try to
       get out cheap (don't bother with coercions etc.). */
    if (v->ob_type == w->ob_type && !PyInstance_Check(v)) {
        cmpfunc fcmp;
        richcmpfunc frich = RICHCOMPARE(v->ob_type);
        /* If the type has richcmp, try it first.  try_rich_compare
           tries it two-sided, which is not needed since we've a
           single type only. */
        if (frich != NULL) {
            /****************************************************/
            /* 1. This first tries v.__eq__(w) then w.__eq__(v) */
            /****************************************************/
            res = (*frich)(v, w, op);
            if (res != Py_NotImplemented)
                goto Done;
            Py_DECREF(res);
        }
        /* No richcmp, or this particular richmp not implemented.
           Try 3-way cmp. */
        fcmp = v->ob_type->tp_compare;
        if (fcmp != NULL) 
            /***********************************************/
            /* Skipped because you don't implement __cmp__ */
            /***********************************************/
            int c = (*fcmp)(v, w);
            c = adjust_tp_compare(c);
            if (c == -2) {
                res = NULL;
                goto Done;
            }
            res = convert_3way_to_object(op, c);
            goto Done;
        }
    }

    /* Fast path not taken, or couldn't deliver a useful result. */
    res = do_richcmp(v, w, op);
Done:
    Py_LeaveRecursiveCall();
    return res;
}

/* Try a genuine rich comparison, returning an object.  Return:
   NULL for exception;
   NotImplemented if this particular rich comparison is not implemented or
     undefined;
   some object not equal to NotImplemented if it is implemented
     (this latter object may not be a Boolean).
*/
static PyObject *
try_rich_compare(PyObject *v, PyObject *w, int op)
{
    richcmpfunc f;
    PyObject *res;

    if (v->ob_type != w->ob_type &&
        PyType_IsSubtype(w->ob_type, v->ob_type) &&
        (f = RICHCOMPARE(w->ob_type)) != NULL) {
            /*******************************************************************************/
            /* Skipped because you don't compare unequal classes where w is a subtype of v */
            /*******************************************************************************/
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = RICHCOMPARE(v->ob_type)) != NULL) {
            /*****************************************************************/
            /** 2. This again tries to evaluate v.__eq__(w) then w.__eq__(v) */
            /*****************************************************************/
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = RICHCOMPARE(w->ob_type)) != NULL) {
            /***********************************************************************/
            /* 3. This tries the reversed comparison: w.__eq__(v) then v.__eq__(w) */
            /***********************************************************************/
        return (*f)(w, v, _Py_SwappedOp[op]);
    }
    res = Py_NotImplemented;
    Py_INCREF(res);
    return res;
}

The interesting parts are the comments - which answers your question: 有趣的部分是评论-可以回答您的问题:

  1. If both are the same type and new-style classes it assumes it can do a shortcut: it makes one attempt to rich compare them. 如果两者都是相同的类型和新样式的类,则假定它可以做一个捷径:它尝试进行丰富的比较。 The normal and reversed return NotImplemented and it proceeds. 正常返回和反向返回NotImplemented并继续执行。

  2. It enters the try_rich_compare function, there it tries to compare them again, first normal then reversed. 它进入try_rich_compare函数,在那里尝试再次比较它们,首先是正常值,然后是相反的值。

  3. A final attempt is made by testing the reversed operation: Now it compares it reversed and then attempts the normal (reversed of the reversed operation) one again. 通过测试反向操作进行最后一次尝试:现在,它比较反向操作,然后再次尝试正常(反向操作的反向操作)。

  4. (not shown) In the end all 3 possibilities failed then a last test is done if the objects are identical a1 is a2 which returns the observed False . (未显示)最后,所有3种可能性都失败了,如果对象相同,则最后一次测试完成a1 is a2 ,它返回观察到的False

The presence of the last test can be observed if you test a1 == a1 : 如果测试a1 == a1则可以观察到最后一个测试的存在:

>>> a1 == a1
a1.__eq__(a1)
a1.__eq__(a1)
a1.__eq__(a1)
a1.__eq__(a1)
a1.__eq__(a1)
a1.__eq__(a1)
True

I don't know if that behaviour is fully documented, at least there are some hints in the documentation of __eq__ 我不知道该行为是否已得到充分记录,至少在__eq__的文档中有一些提示

A rich comparison method may return the singleton NotImplemented if it does not implement the operation for a given pair of arguments. 如果富比较方法未实现给定参数对的操作,则它可能返回单例NotImplemented。

and __cmp__ : __cmp__

Called by comparison operations if rich comparison (see above) is not defined. 如果未定义丰富比较(请参见上文),则由比较操作调用。


Some more observations: 其他一些观察:

Note that if you define __cmp__ it doesn't respect return NotImplemented like __eq__ does (because it enters the previously skipped branch in PyObject_RichCompare ): 请注意,如果您定义__cmp__它将不像__eq__那样尊重return NotImplemented (因为它在PyObject_RichCompare输入了先前跳过的分支):

class A(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name
    def __eq__(self, other):
        print('{}.__eq__({})'.format(self, other))
        return NotImplemented
    def __cmp__(self, other):
        print('{}.__cmp__({})'.format(self, other))
        return NotImplemented


>>> a1, a2 = A('a1'), A('a2')
>>> a1 == a2
a1.__eq__(a2)
a2.__eq__(a1)
a1.__cmp__(a2)
a2.__cmp__(a1)
False

The behaviour with the subclasses or identical classes can be seen easily seen if you explicitly compare with superclass and inherited class: 如果您显式地与超类和继承的类进行比较,则可以轻松看出子类或相同类的行为:

>>> class A(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return self.name
...     def __eq__(self, other):
...         print('{}.__eq__({}) from A'.format(self, other))
...         return NotImplemented
...
>>>
>>> class B(A):
...     def __eq__(self, other):
...         print('{}.__eq__({}) from B'.format(self, other))
...         return NotImplemented
...
>>>
>>> a1, a2 = A('a1'), B('a2')
>>> a1 == a2
a2.__eq__(a1) from B
a1.__eq__(a2) from A
a1.__eq__(a2) from A
a2.__eq__(a1) from B
a2.__eq__(a1) from B
a1.__eq__(a2) from A
False
>>> a2 == a1
a2.__eq__(a1) from B
a1.__eq__(a2) from A
a1.__eq__(a2) from A
a2.__eq__(a1) from B
False

A final comment: 最后的评论:

I added the code I used to "print" where it does the comparisons in a gist . 我添加了我用来“打印” 要点的代码 If you know how to create python-c-extensions you can compile and run the code yourself (the myrichcmp function needs to be called with the two arguments to compare for equality). 如果您知道如何创建python-c-extensions,则可以自己编译并运行代码(需要使用两个参数调用myrichcmp函数以进行相等性比较)。

Please read Python Manual about NotImplemented: "Numeric methods and rich comparison methods should return this value if they do not implement the operation for the operands provided. (The interpreter will then try the reflected operation, or some other fallback, depending on the operator.) Its truth value is true." 请阅读有关NotImplemented的Python手册:“如果数字方法和丰富比较方法未实现所提供的操作数的操作,则应返回此值。(然后,解释程序将尝试使用反射操作或其他后备方式,具体取决于运算符。 )其真实价值是真实的。”

That is if you change return NotImplemented by return self.name==other.name, there will be a single call to eq 也就是说,如果您通过return self.name == other.name更改return NotImplemented,将只有一个对eq的调用

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM