Подтвердить что ты не робот

Python a == b вызывает b.__ eq __ (a) для подкласса без переопределения

В python 2.7.6 предположим, что у меня есть класс, который определяет __eq__ и подкласс его:

>>> class A(object):
...     def __eq__(self,other):
...         print self.__class__,other.__class__
...         return True
... 
>>> class B(A):
...     pass
... 

Теперь я создаю объект каждого класса и хочу сравнить его:

>>> a = A()
>>> b = B()
>>> a==b

Результат:

<class '__main__.B'> <class '__main__.A'>

Это показывает, что интерпретатор вызывает b.__eq__(a) вместо a.__eq__(b) как ожидается.

documentation состояния (выделено мной):

  • Для объектов x и y проверяется первая x.__op__(y). Если это не реализовано или возвращает NotImplemented, проверяется y.__rop__(x). Если это также не реализовано или возвращает NotImplemented, возникает исключение TypeError. Но см. Следующее исключение:

  • Исключение к предыдущему элементу: если левый операнд является экземпляром встроенного типа или класса нового стиля, а правый операнд является экземпляром соответствующего подкласса этого типа или класса и переопределяет метод оснований __rop__(), метод правых операндов __rop__() проверяется перед левым операндом __op__().

    Это делается для того, чтобы подкласс мог полностью переопределить двоичные операторы. В противном случае метод левых операндов __op__() всегда будет принимать правый операнд: когда ожидается экземпляр данного класса, всегда допустим экземпляр подкласса этого класса.

Поскольку подкласс B не переопределяет оператор __eq__, не должен a.__eq__(b) вызывать вместо b.__eq__(a)? Является ли это ожидаемым поведением или ошибкой? Это противоречит документации, когда я ее читал: я неправильно истолковал документацию или пропустил что-то еще?

Некоторые связанные вопросы:

  • Этот ответ цитирует приведенную выше документацию. В этом случае последний вопрос включал сравнение объекта встроенного типа (1) и экземпляра нового стиля. Здесь я специально сравниваю экземпляр родительского класса с экземпляром подкласса, который не переопределяет метод rop() его parent (в этом случае __eq__ имеет значение op() и rop()).

    В этом случае python фактически вызывает b.__eq__(a) вместо a.__eq__(b), хотя класс B явно не переопределяет A.

4b9b3361

Ответ 1

Здесь код, который реализует описанную логику:

Python 2.7:

/* Macro to get the tp_richcompare field of a type if defined */
#define RICHCOMPARE(t) (PyType_HasFeature((t), Py_TPFLAGS_HAVE_RICHCOMPARE) \
             ? (t)->tp_richcompare : NULL)

...

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) {
        res = (*f)(w, v, _Py_SwappedOp[op]);  // We're executing this
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = RICHCOMPARE(v->ob_type)) != NULL) {
        res = (*f)(v, w, op);  // Instead of this.
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    if ((f = RICHCOMPARE(w->ob_type)) != NULL) {
        return (*f)(w, v, _Py_SwappedOp[op]);
    }
    res = Py_NotImplemented;
    Py_INCREF(res);
    return res;
}

Python 3.x:

/* Perform a rich comparison, raising TypeError when the requested comparison
   operator is not supported. */
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]);  // We're executing this
        if (res != Py_NotImplemented)
            return res; 
        Py_DECREF(res);
    }    
    if ((f = v->ob_type->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);   // Instead of this.
        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);
    }    

Две версии аналогичны, за исключением того, что версия Python 2.7 использует макрос RICHCOMPARE, который проверяет PyType_HasFeature((t), Py_TPFLAGS_HAVE_RICHCOMPARE вместо ob_type->tp_richcompare != NULL.

В обеих версиях первый блок if оценивает значение true. Конкретный фрагмент, который, возможно, ожидал бы быть ложным, происходит по описанию в документах: f = w->ob_type->tp_richcompare != NULL (для Py3)/PyType_HasFeature((t), Py_TPFLAGS_HAVE_RICHCOMPARE. Тем не менее, документы говорят, что tp_richcompare наследуется дочерними классами:

richcmpfunc PyTypeObject.tp_richcompare

Необязательный указатель на богатую функцию сравнения...

Это поле наследуется подтипами вместе с tp_compare и tp_hash...

В версии 2.x PyType_HasFeature((t), Py_TPFLAGS_HAVE_RICHCOMPARE также будет оцениваться как true, потому что флаг Py_TPFLAGS_HAVE_RICHCOMPARE имеет значение true, если tp_richcompare, tp_clear и tp_traverse являются истинными, и все они унаследованы от родителя.

Итак, хотя B не предоставляет свой собственный богатый метод сравнения, он все равно возвращает значение, отличное от NULL, потому что его родительский класс предоставляет его. Как утверждали другие, это, похоже, ошибка в документе; дочернему классу на самом деле не нужно переопределять метод __eq__ родителя, он просто должен предоставить один, даже через наследование.

Ответ 2

Похоже, что подкласс считается "переопределить" поведение суперкласса, даже если все, что он делает, наследует поведение суперкласса. Это трудно увидеть в случае __eq__, потому что __eq__ является его собственным отражением, но вы можете увидеть его более четко, если вы используете разные операторы, такие как __lt__ и __gt__, которые являются друг другом отражениями:

class A(object):
    def __gt__(self,other):
        print "GT", self.__class__, other.__class__

    def __lt__(self,other):
        print "LT", self.__class__, other.__class__

class B(A):
    pass

Тогда:

>>> A() > B()
LT <class '__main__.B'> <class '__main__.A'>

Обратите внимание, что A.__gt__ не был вызван; вместо этого вызывается B.__lt__.

Документация Python 3 иллюстрирует это тем, что в ней указано правило в технически более точных словах (выделено мной):

Если правый тип операндов является подклассом типа левых операндов, а этот подкласс предоставляет отраженный метод для операции, этот метод будет вызываться перед левым операндом без отраженного метода. Такое поведение позволяет подклассам переопределять операции своих предков.

Подкласс действительно "обеспечивает" отраженный метод, он просто предоставляет его через наследование. Если вы фактически удаляете поведение отраженного метода в подклассе (возвращая NotImplemented), метод суперкласса правильно вызывается (после подкласса один):

class A(object):
    def __gt__(self,other):
        print "GT", self.__class__, other.__class__

    def __lt__(self,other):
        print "LT", self.__class__, other.__class__

class B(A):
    def __lt__(self, other):
        print "LT", self.__class__, other.__class__
        return NotImplemented

>>> A() > B()
LT <class '__main__.B'> <class '__main__.A'>
GT <class '__main__.A'> <class '__main__.B'>

Таким образом, в основном это ошибка документации. Он должен сказать, что метод отражения подкласса всегда сначала проверяется (для операторов сравнения), независимо от того, явно ли переопределяет подкласс класс суперкласса. (Как заметил Марк Дикинсон в комментарии, однако, он работает только таким образом для операторов сравнения, а не для пар математических операторов, таких как __add__/__radd__.)

На практике это вряд ли имеет значение, поскольку единственный раз, когда вы замечаете это, когда подкласс не переопределяет суперкласс. Но в этом случае поведение подкласса по определению так же, как и суперкласса, так или иначе, поэтому на самом деле не имеет значения, какой из них вызывается (если вы не делаете что-то опасное, как мутирование объекта из метода сравнения, и в этом случае вы все равно должны были быть начеку).