Jak obsługiwane jest __eq__ w Pythonie i w jakiej kolejności?

106

Ponieważ Python nie udostępnia lewej / prawej wersji swoich operatorów porównania, w jaki sposób decyduje, którą funkcję wywołać?

class A(object):
    def __eq__(self, other):
        print "A __eq__ called"
        return self.value == other
class B(object):
    def __eq__(self, other):
        print "B __eq__ called"
        return self.value == other

>>> a = A()
>>> a.value = 3
>>> b = B()
>>> b.value = 4
>>> a == b
"A __eq__ called"
"B __eq__ called"
False

To wydaje się wywoływać obie __eq__funkcje.

Szukam oficjalnego drzewa decyzyjnego.

PyProg
źródło

Odpowiedzi:

126

a == bWyrażenie powołuje A.__eq__, gdyż istnieje. Jego kod zawiera self.value == other. Ponieważ int nie wiedzą, jak porównać się do typu B, Python próbuje wywołać, B.__eq__aby sprawdzić, czy wie, jak porównać się z int.

Jeśli zmienisz kod, aby pokazać, jakie wartości są porównywane:

class A(object):
    def __eq__(self, other):
        print("A __eq__ called: %r == %r ?" % (self, other))
        return self.value == other
class B(object):
    def __eq__(self, other):
        print("B __eq__ called: %r == %r ?" % (self, other))
        return self.value == other

a = A()
a.value = 3
b = B()
b.value = 4
a == b

wydrukuje:

A __eq__ called: <__main__.A object at 0x013BA070> == <__main__.B object at 0x013BA090> ?
B __eq__ called: <__main__.B object at 0x013BA090> == 3 ?
Ned Batchelder
źródło
69

Kiedy Python2.x widzi a == b, próbuje wykonać następujące czynności.

  • Jeśli type(b)jest to klasa w nowym stylu, type(b)jest podklasą type(a)i type(b)nadpisano __eq__, to wynikiem jest b.__eq__(a).
  • Jeśli type(a)został przesłonięty __eq__(to type(a).__eq__znaczy nie jest object.__eq__), to wynik jest a.__eq__(b).
  • Jeśli type(b)została zastąpiona __eq__, wynikiem jest b.__eq__(a).
  • Jeśli nie ma żadnego z powyższych przypadków, Python powtarza poszukiwany proces __cmp__. Jeśli istnieje, obiekty są równe, jeśli zwraca zero.
  • Jako ostateczne rozwiązanie awaryjne, wywołania Pythona object.__eq__(a, b), które są Trueiff ai bsą tym samym obiektem.

Jeśli którakolwiek z metod specjalnych zwróci NotImplemented, Python zachowuje się tak, jakby metoda nie istniała.

Zwróć uwagę na ostatni krok ostrożnie: jeśli ani anie jest bprzeciążony ==, ani nie , to a == bjest to to samo, co a is b.


Od https://eev.ee/blog/2012/03/24/python-faq-equality/

kev
źródło
1
Wygląda na to, że dokumentacja Pythona 3 była niepoprawna. Zobacz bugs.python.org/issue4395 i poprawkę dla wyjaśnienia. TLDR: podklasa nadal jest porównywana jako pierwsza, nawet jeśli jest na prawej stronie.
maksymalnie
Cześć kev, niezły post. Czy możesz wyjaśnić, gdzie udokumentowano pierwszy punktor i dlaczego został tak zaprojektowany?
wim
1
Tak, gdzie jest to udokumentowane dla Pythona 2? Czy to jest PEP?
Mr_and_Mrs_D
Bazując na tej odpowiedzi i towarzyszących jej komentarzach, wprawiło mnie to w większe zakłopotanie niż wcześniej.
Sajuuk
i btw, czy zdefiniowanie metody związanej __eq__ tylko na wystąpieniu jakiegoś typu nie jest wystarczające do zastąpienia ==?
Sajuuk
11

Piszę zaktualizowaną odpowiedź dla Pythona 3 na to pytanie.

Jak jest __eq__obsługiwane w Pythonie i w jakiej kolejności?

a == b

Powszechnie wiadomo, ale nie zawsze tak jest, że a == bwywołuje a.__eq__(b)lub type(a).__eq__(a, b).

Mówiąc wprost, kolejność oceny jest następująca:

  1. jeśli btyp jest ścisłą podklasą (nie tym samym typem) tego atypu i ma __eq__znak, wywołaj go i zwróć wartość, jeśli porównanie jest zaimplementowane,
  2. w przeciwnym razie, jeśli ama __eq__, wywołaj go i zwróć, jeśli porównanie jest zaimplementowane,
  3. inaczej, zobacz, czy nie wywołaliśmy b __eq__i ma to, a następnie wywołaj i zwróć, jeśli porównanie jest zaimplementowane,
  4. w przeciwnym razie wykonaj porównanie tożsamości, to samo porównanie co is.

Wiemy, czy porównanie nie jest zaimplementowane, jeśli metoda zwraca NotImplemented.

(W Pythonie 2 była __cmp__metoda, której szukano, ale została wycofana i usunięta w Pythonie 3.)

Przetestujmy dla siebie zachowanie pierwszego sprawdzenia, pozwalając na podklasę B A, która pokazuje, że zaakceptowana odpowiedź jest błędna pod tym względem:

class A:
    value = 3
    def __eq__(self, other):
        print('A __eq__ called')
        return self.value == other.value

class B(A):
    value = 4
    def __eq__(self, other):
        print('B __eq__ called')
        return self.value == other.value

a, b = A(), B()
a == b

które drukuje tylko B __eq__ calledprzed powrotem False.

Skąd znamy ten pełny algorytm?

Inne odpowiedzi tutaj wydają się niekompletne i nieaktualne, więc zaktualizuję informacje i pokażę, jak możesz to sprawdzić.

Jest to obsługiwane na poziomie C.

Musimy przyjrzeć się tutaj dwóm różnym bitom kodu - domyślnemu __eq__dla obiektów klasy objectoraz kodowi, który wyszukuje i wywołuje __eq__metodę niezależnie od tego, czy używa domyślnej, __eq__czy niestandardowej.

Domyślna __eq__

Wyszukiwanie __eq__w odpowiednich dokumentach C api pokazuje, że __eq__jest obsługiwane przez tp_richcompare- które w "object"definicji typu w cpython/Objects/typeobject.cjest zdefiniowane w object_richcomparefor case Py_EQ:.

    case Py_EQ:
        /* Return NotImplemented instead of False, so if two
           objects are compared, both get a chance at the
           comparison.  See issue #1393. */
        res = (self == other) ? Py_True : Py_NotImplemented;
        Py_INCREF(res);
        break;

Więc tutaj, jeśli self == otherwrócimy True, w przeciwnym razie zwrócimy NotImplementedobiekt. Jest to domyślne zachowanie dla dowolnej podklasy obiektu, która nie implementuje własnej __eq__metody.

Jak __eq__się nazywa

Następnie znajdujemy dokumentację C API, funkcję PyObject_RichCompare , która wywołuje do_richcompare.

Następnie widzimy, że tp_richcomparefunkcja utworzona dla "object"definicji C jest wywoływana przez do_richcompare, więc przyjrzyjmy się temu trochę dokładniej.

Pierwsza kontrola w tej funkcji dotyczy warunków porównywanych obiektów:

  • nie są tego samego typu, ale
  • drugi typ jest podklasą pierwszego typu, a
  • drugi typ ma __eq__metodę,

następnie wywołaj metodę drugiej osoby z zamienionymi argumentami, zwracając wartość, jeśli jest zaimplementowana. Jeśli ta metoda nie zostanie zaimplementowana, kontynuujemy ...

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

Następnie sprawdzamy, czy możemy wyszukać __eq__metodę z pierwszego typu i wywołać ją. Dopóki wynik nie jest NotImplemented, czyli jest zaimplementowany, zwracamy go.

    if ((f = Py_TYPE(v)->tp_richcompare) != NULL) {
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);

W przeciwnym razie, jeśli nie wypróbowaliśmy metody innego typu, a ona jest, wtedy ją wypróbowujemy, a jeśli porównanie jest zaimplementowane, zwracamy je.

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

Na koniec otrzymujemy rezerwę na wypadek, gdyby nie została zaimplementowana dla żadnego typu.

Fallback sprawdza tożsamość obiektu, czyli czy jest to ten sam obiekt w tym samym miejscu w pamięci - to jest to samo sprawdzenie co dla self is other:

    /* 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;

Wniosek

W porównaniu najpierw szanujemy implementację porównania podklas.

Następnie próbujemy porównać z implementacją pierwszego obiektu, a następnie z implementacją drugiego, jeśli nie został wywołany.

Na koniec używamy testu tożsamości do porównania równości.

Aaron Hall
źródło