Почему в 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__и т. д.

Другие вопросы по тегам