【问题标题】:How exactly does Python operator overloading work?Python 运算符重载究竟是如何工作的?
【发布时间】:2017-01-11 21:09:50
【问题描述】:

我尝试了以下实验:

>>> 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

这到底是怎么回事?

更一般地说,是否有关于评估操作员时发生的确切流程的官方文档? data model 文档暗示了某种回退行为,但没有准确描述它是什么。有几种可能性会使情况复杂化:

  • ab 可以是相同或不同的类型
  • ab 可能会也可能不会定义 __eq__ 方法
  • a.__eq__(b)b.__eq__(a) 可能返回 NotImplemented 或其他值。

某种流程图会很有帮助。


编辑:在将此问题标记为重复之前,请确保假定的重复回答以下问题:

  • 为什么__eq__ 在给定模式中被调用了 6 次?
  • 在哪里完整记录了该行为?
  • 我可以获得流程图吗?

【问题讨论】:

    标签: python python-2.7 class operator-overloading


    【解决方案1】:

    这种行为仅适​​用于 python-2.x,它是丰富的比较在内部工作的一部分(至少是 CPython),但前提是两者都是新型类并且两个参数具有相同的类型!

    源 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;
    }
    

    有趣的部分是 cmets - 它回答了您的问题:

    1. 如果两者是相同的类型和新样式的类,它假定它可以做一个捷径:它会尝试对它们进行丰富的比较。正常和反向返回 NotImplemented 并继续。

    2. 它进入try_rich_compare函数,尝试再次比较它们,先正常然后反转。

    3. 通过测试反向操作进行最后一次尝试:现在它比较反向操作,然后再次尝试正常(反向操作的反向操作)。

    4. (未显示)最后所有 3 种可能性都失败了,如果对象相同 a1 is a2,则完成最后一次测试,返回观察到的 False

    如果你测试a1 == a1,可以观察到最后一个测试的存在:

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

    我不知道这种行为是否被完整记录,至少__eq__的文档中有一些提示

    如果一个富比较方法没有为给定的一对参数实现操作,它可能会返回单例 NotImplemented。

    __cmp__:

    如果没有定义丰富的比较(见上文),则由比较操作调用。


    更多观察:

    请注意,如果您定义__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
    

    如果您明确地与超类和继承类进行比较,可以很容易地看到子类或相同类的行为:

    >>> 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
    

    最后的评论:

    我在gist 中添加了用于“打印”的代码,用于进行比较。如果你知道如何创建 python-c-extensions,你可以自己编译和运行代码(myrichcmp 函数需要调用两个参数来比较是否相等)。

    【讨论】:

    • 很好的解释。正如您所注意到的,最后四个比较是由文档定义的:首先尝试从右侧开始的反向比较(如果 RHS 返回 NotImplemented,这可能导致从左侧尝试)然后尝试从左侧开始的普通比较(可能如果左侧返回 NotImplemented,则导致从右侧尝试)。前两个是优化。我认为一个重要的收获(尽管文档中没有明确说明)是调用比较函数的确切次数是一个实现细节,而不是定义的行为
    • @augurar 我已将所有相关源代码包含在问题中,并突出显示了相关比较及其顺序。如果我需要解释其他任何事情,请告诉我:)(但你不会得到流程图 xD)
    • 另外值得注意的是,如果一个对象没有定义任何丰富的比较方法,那么另一个对象的__eq__方法只会被调用一次,不会被调用两次。
    【解决方案2】:

    请阅读关于 NotImplemented 的 Python 手册:“如果数值方法和富比较方法没有实现所提供操作数的操作,则应返回此值。(解释器将尝试反射操作或其他一些回退,具体取决于运算符。)它的真值是真的。”

    也就是说,如果您将 return NotImplemented 更改为 return self.name==other.name,将有一个对 eq

    的调用

    【讨论】:

    • 这没有回答问题。我的问题是:为什么会发生观察到的行为?
    • 我已经写过:因为您从代码中返回了 NotImplemented,所以“解释器将尝试反射操作或其他一些回退,具体取决于操作员。”为什么是6次?因为您使用的是 Python 2.7。 Python 3.5.2 和 3.6.0 仅尝试 2 个比较变体。可能是 Python 2.7 中的一个错误。
    猜你喜欢
    • 2020-05-26
    • 1970-01-01
    • 1970-01-01
    • 2010-12-24
    • 1970-01-01
    • 1970-01-01
    • 2013-05-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多