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
.
Ответы
Ответ 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__
.)
На практике это вряд ли имеет значение, поскольку единственный раз, когда вы замечаете это, когда подкласс не переопределяет суперкласс. Но в этом случае поведение подкласса по определению так же, как и суперкласса, так или иначе, поэтому на самом деле не имеет значения, какой из них вызывается (если вы не делаете что-то опасное, как мутирование объекта из метода сравнения, и в этом случае вы все равно должны были быть начеку).