Почему в Python нет метода "__req__" (отраженного равенства)?
У меня есть маленький вспомогательный класс:
class AnyOf(object):
def __init__(self, *args):
self.elements = args
def __eq__(self, other):
return other in self.elements
Это позволяет мне делать сладкую магию, как:
>>> arr = np.array([1,2,3,4,5])
>>> arr == AnyOf(2,3)
np.array([False, True, True, False, False])
без использования понимания списка (как в np.array(x in (2,3) for x in arr
).
(Я поддерживаю пользовательский интерфейс, который позволяет (доверенным) пользователям вводить произвольный код, и a == AnyOf(1,2,3)
гораздо более приемлемым, чем понимание списка для не технически подкованного пользователя.)
Тем не мение!
Это работает только в одну сторону! Например, если бы я должен был сделать AnyOf(2,3) == arr
тогда мой AnyOf
класса __eq__
метод никогда не вызывается: вместо этого массив NumPy __eq__
вызывается метод, который внутренне (я бы предположил) вызывает __eq__
метод всех его элементов.
Это заставляет меня задуматься: почему Python не допускает правосторонний эквивалент __eq__
? (Примерно эквивалентно таким методам, как __radd__
, __rmul__
и так далее.)
4 ответа
__req__
не очень хорошая идея на языке, потому что если класс Left
определяет __eq__
и класс Right
определяет __req__
то Python обязан принять последовательное решение о том, кто будет вызван первым Left() == Right()
, Они не могут оба победить.
Тем не менее, модель данных Python позволяет вам делать то, что вы хотите здесь. С обеих сторон вы можете контролировать это сравнение, но вам нужно определить AnyOf
правильно. Если ты хочешь AnyOf
чтобы управлять __eq__ с правой стороны, вы должны определить его как подкласс np.ndarray
,
если бы я должен был сделать
AnyOf(2,3) == arr
тогда мойAnyOf
класса__eq__
метод никогда не вызывается
Нет, у вас есть фундаментальное недоразумение здесь. Левая часть всегда получает первую попытку при сравнении на равенство, если только правая часть не является подклассом типа левой части.
arr == AnyOf(2,3)
В случае выше, ваш обычай __eq__
вызывается, потому что это вызывает массив numpy! Так np.ndarray
выигрывает, и он решает проверить один раз за элемент. Он буквально может сделать что-нибудь еще, в том числе не вызывая ваш AnyOf.__eq__
совсем.
AnyOf(2,3) == arr
В приведенном выше случае ваш класс получает первую попытку при сравнении, и он терпит неудачу из-за того, как вы использовали in
(проверка, находится ли массив в кортеже).
Это документация по модели данных:
Не существует версий этих методов со свопированными аргументами (которые будут использоваться, когда левый аргумент не поддерживает операцию, но правый аргумент поддерживает); скорее,
__lt__()
а также__gt__()
являются отражением друг друга,__le__()
а также__ge__()
являются отражением друг друга, и__eq__()
а также__ne__()
являются их собственным отражением. Если операнды имеют разные типы, а тип правого операнда является прямым или косвенным подклассом типа левого операнда, отраженный метод правого операнда имеет приоритет, в противном случае метод левого операнда имеет приоритет. Виртуальные подклассы не рассматриваются.
Как указано в комментариях выше, что вы хотите работает, и __eq__
по сути то же самое, что и потенциал __req__
: он вызывается справа от ==
если объект с левой стороны возвращается NotImplemented
:
In [1]: class A:
...: def __eq__(self, other):
...: return NotImplemented
...:
In [2]: class B:
...: def __eq__(self, other):
...: print("B comparing")
...: return True
...:
In [3]: B() == A()
B comparing
Out[3]: True
In [4]: A() == B()
B comparing
Out[4]: True
In [5]: A() == A()
Out[5]: False
Оказывается, он работает даже с другими, обычными объектами:
In [10]: 5 == B()
B comparing
Out[10]: True
Тем не менее, некоторые объекты могут привести к TypeError на __eq__
вместо возвращения NotImplemented
или же False
и это делает это не надежным для всех видов объектов.
Что происходит в вашем случае, это неправильное использование оператора in
с массивами и кортежами внутри вашего собственного __eq__
метод. (Спасибо @wim, что заметил это в другом ответе здесь).
Документация о __rxx__
методы, такие как __radd__
состояния:
Эти функции вызываются только в том случае, если левый операнд не поддерживает соответствующую операцию и операнды бывают разных типов.
Пока классов нет __add__
или же __sub__
методы по умолчанию, они имеют __eq__
:
>>> class A(object):
... pass
>>> '__eq__' in dir(A)
True
Это означает __req__
никогда не будет вызван, если вы явно не удалите __eq__
из другого класса.
Вы можете решить свою конкретную проблему с np.in1d
:
>>> np.in1d(arr, [2, 3])
array([False, True, True, False, False], dtype=bool)
Я хотел точно знать, что делает интерпретатор CPython, когда встречаетa == b
.
import dis
def eq(a, b):
return a == b
print(dis.dis(eq))
Это дает вам:
1 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 COMPARE_OP 2 (==)
6 RETURN_VALUE
Итак, он используетCOMPARE_OP
оп. Вот код, который это обрабатывает. (Кстати, если посмотреть на более ранние версии CPython, код оценочного кадра был намного проще и за ним было гораздо легче следовать. Теперь он сильно оптимизирован для скорости. Я смотрю на текущий мастер здесь.)
Вы видите, что здесь используетсяPyObject_RichCompare
функция. Мы находим это здесь .
Соответствующая логика такова:
/* For Python 3.0.1 and later, the old three-way comparison has been
completely removed in favour of rich comparisons. PyObject_Compare() and
PyObject_Cmp() are gone, and the builtin cmp function no longer exists.
The old tp_compare slot has been renamed to tp_as_async, and should no
longer be used. Use tp_richcompare instead.
See (*) below for practical amendments.
tp_richcompare gets called with a first argument of the appropriate type
and a second object of an arbitrary type. We never do any kind of
coercion.
The tp_richcompare slot should return an object, as follows:
NULL if an exception occurred
NotImplemented if the requested comparison is not implemented
any other false value if the requested comparison is false
any other true value if the requested comparison is true
The PyObject_RichCompare[Bool]() wrappers raise TypeError when they get
NotImplemented.
(*) Practical amendments:
- If rich comparison returns NotImplemented, == and != are decided by
comparing the object pointer (i.e. falling back to the base object
implementation).
*/
/* Map rich comparison operators to their swapped version, e.g. LT <--> GT */
int _Py_SwappedOp[] = {Py_GT, Py_GE, Py_EQ, Py_NE, Py_LT, Py_LE};
static const char * const opstrings[] = {"<", "<=", "==", "!=", ">", ">="};
/* Perform a rich comparison, raising TypeError when the requested comparison
operator is not supported. */
static PyObject *
do_richcompare(PyThreadState *tstate, PyObject *v, PyObject *w, int op)
{
richcmpfunc f;
PyObject *res;
int checked_reverse_op = 0;
if (!Py_IS_TYPE(v, Py_TYPE(w)) &&
PyType_IsSubtype(Py_TYPE(w), Py_TYPE(v)) &&
(f = Py_TYPE(w)->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 = Py_TYPE(v)->tp_richcompare) != NULL) {
res = (*f)(v, w, op);
if (res != Py_NotImplemented)
return res;
Py_DECREF(res);
}
if (!checked_reverse_op && (f = Py_TYPE(w)->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(tstate, PyExc_TypeError,
"'%s' not supported between instances of '%.100s' and '%.100s'",
opstrings[op],
Py_TYPE(v)->tp_name,
Py_TYPE(w)->tp_name);
return NULL;
}
return Py_NewRef(res);
}
Это показывает, что ты действительно можешь вернутьсяNotImplemented
в вашем и как это обрабатывается.
tp_richcompare
может быть перезаписан пользовательским типом. Но в случае по умолчанию это должно быть так . Итак, как и ожидалось, обычно это должно вызывать__eq__
и т. д.