Czy powinienem zaimplementować __ne__ pod względem __eq__ w Pythonie?

101

Mam klasę, w której chcę zastąpić __eq__metodę. Wydaje się sensowne, że powinienem zastąpić __ne__metodę jak dobrze, ale czy to ma sens, aby wdrożyć __ne__w kategoriach __eq__jako takich?

class A:

    def __init__(self, attr):
        self.attr = attr

    def __eq__(self, other):
        return self.attr == other.attr
    
    def __ne__(self, other):
        return not self.__eq__(other)

A może jest coś, czego mi brakuje w sposobie, w jaki Python używa tych metod, co sprawia, że ​​nie jest to dobry pomysł?

Falmarri
źródło

Odpowiedzi:

60

Tak, w porządku. W rzeczywistości dokumentacja zachęca do __ne__zdefiniowania __eq__:

Nie ma domniemanych relacji między operatorami porównania. Prawda x==ynie oznacza, że x!=y jest fałszywa. W związku z tym podczas definiowania __eq__()należy również zdefiniować __ne__()tak, aby operatorzy zachowywali się zgodnie z oczekiwaniami.

W wielu przypadkach (takich jak ten) będzie to tak proste, jak zanegowanie wyniku __eq__, ale nie zawsze.

Daniel DiPaolo
źródło
12
to jest właściwa odpowiedź (tutaj, przez @ aaron-hall). Cytowana przez Ciebie dokumentacja nie zachęca do implementacji __ne__using __eq__, a jedynie do jej wdrożenia.
guyarad
2
@guyarad: Właściwie, odpowiedź Aarona jest nadal trochę błędna, ponieważ nie została prawidłowo delegowana; zamiast traktować NotImplementedpowrót z jednej strony jako wskazówkę do delegowania __ne__z drugiej strony, not self == otherjest (zakładając, że operand __eq__nie wie, jak porównać inny operand), niejawnie deleguje na __eq__drugą stronę, a następnie odwraca. W przypadku dziwnych typów, np. Pól ORM SQLAlchemy, powoduje to problemy .
ShadowRanger
1
Krytyka ShadowRanger dotyczyłaby tylko bardzo patologicznych przypadków (IMHO) i została w pełni omówiona w mojej odpowiedzi poniżej.
Aaron Hall
2
Nowsze dokumentacje (przynajmniej dla wersji 3.7, mogą być nawet wcześniejsze) są __ne__automatycznie przekazywane, __eq__a cytat w tej odpowiedzi nie istnieje już w docs. Podsumowując, wdrażanie tylko __eq__i zezwalanie na __ne__delegowanie jest całkowicie pythonowe .
bluesummers
134

Python, czy powinienem zaimplementować __ne__()operator oparty na __eq__?

Krótka odpowiedź: Nie wdrażaj tego, ale jeśli musisz ==, nie używaj__eq__

W Pythonie 3 !=jest negacją ==domyślnie, więc nie musisz nawet pisać a __ne__, a dokumentacja nie jest już zdania na temat pisania takiego.

Ogólnie rzecz biorąc, w przypadku kodu tylko w Pythonie 3 nie pisz go, chyba że musisz przyćmić implementację rodzica, np. Dla wbudowanego obiektu.

To znaczy, pamiętaj o komentarzu Raymonda Hettingera :

__ne__Sposób następuje automatycznie __eq__tylko wtedy, gdy __ne__nie jest już zdefiniowane w nadrzędnej. Jeśli więc dziedziczysz z wbudowanego, najlepiej zastąpić oba.

Jeśli chcesz, aby Twój kod działał w Pythonie 2, postępuj zgodnie z zaleceniami dla Pythona 2 i będzie działał w Pythonie 3.

W Pythonie 2, sam Python nie implementuje automatycznie żadnej operacji w kategoriach innej - dlatego powinieneś zdefiniować __ne__w kategoriach ==zamiast __eq__. NA PRZYKŁAD

class A(object):
    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return not self == other # NOT `return not self.__eq__(other)`

Zobacz dowód

  • __ne__()operator wdrażający oparty na __eq__i
  • w ogóle nie implementuje __ne__w Pythonie 2

zapewnia nieprawidłowe zachowanie w poniższej demonstracji.

Długa odpowiedź

Dokumentacji dla Pythona 2 mówi:

Nie ma domniemanych relacji między operatorami porównania. Prawda x==ynie oznacza, że x!=yjest fałszywa. W związku z tym podczas definiowania __eq__()należy również zdefiniować __ne__()tak, aby operatorzy zachowywali się zgodnie z oczekiwaniami.

Oznacza to, że jeśli zdefiniujemy __ne__w kategoriach odwrotności do __eq__, możemy uzyskać spójne zachowanie.

Ta sekcja dokumentacji została zaktualizowana dla języka Python 3:

Domyślnie __ne__()deleguje __eq__()i odwraca wynik, chyba że tak jest NotImplemented.

aw sekcji „co nowego” widzimy, że zmieniło się to zachowanie:

  • !=teraz zwraca przeciwieństwo ==, chyba że ==zwraca NotImplemented.

Do implementacji __ne__wolimy używać ==operatora zamiast bezpośrednio używać __eq__metody, więc jeśli self.__eq__(other)podklasa zwróci NotImplementeddla sprawdzonego typu, Python odpowiednio sprawdzi other.__eq__(self) Z dokumentacji :

NotImplementedprzedmiot

Ten typ ma jedną wartość. Istnieje jeden obiekt o tej wartości. Dostęp do tego obiektu uzyskuje się za pośrednictwem wbudowanej nazwy NotImplemented. Metody numeryczne i bogate metody porównania mogą zwracać tę wartość, jeśli nie implementują operacji dla podanych operandów. (W zależności od operatora interpreter spróbuje wykonać odzwierciedloną operację lub inną rezerwę). Jego wartość prawda to prawda.

Kiedy podano bogaty operator porównania, jeśli nie są one tego samego typu, Python sprawdza czy otherjest podtypem, a jeśli ma to operator zdefiniowany, używa otherpierwszy „s metody (odwrotność do <, <=, >=i >). Jeśli NotImplementedjest zwracany, a następnie wykorzystuje metodę Przeciwieństwem jest. (To ma nie sprawdzić tej samej metody dwa razy). Za pomocą ==operatora pozwala na to logika się odbyć.


Oczekiwania

Z semantycznego __ne__punktu widzenia należy zaimplementować w zakresie sprawdzania równości, ponieważ użytkownicy Twojej klasy będą oczekiwać, że następujące funkcje będą równoważne dla wszystkich wystąpień A .:

def negation_of_equals(inst1, inst2):
    """always should return same as not_equals(inst1, inst2)"""
    return not inst1 == inst2

def not_equals(inst1, inst2):
    """always should return same as negation_of_equals(inst1, inst2)"""
    return inst1 != inst2

Oznacza to, że obie powyższe funkcje powinny zawsze zwracać ten sam wynik. Ale to zależy od programisty.

Demonstracja nieoczekiwanego zachowania podczas definiowania __ne__na podstawie __eq__:

Najpierw konfiguracja:

class BaseEquatable(object):
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return isinstance(other, BaseEquatable) and self.x == other.x

class ComparableWrong(BaseEquatable):
    def __ne__(self, other):
        return not self.__eq__(other)

class ComparableRight(BaseEquatable):
    def __ne__(self, other):
        return not self == other

class EqMixin(object):
    def __eq__(self, other):
        """override Base __eq__ & bounce to other for __eq__, e.g. 
        if issubclass(type(self), type(other)): # True in this example
        """
        return NotImplemented

class ChildComparableWrong(EqMixin, ComparableWrong):
    """__ne__ the wrong way (__eq__ directly)"""

class ChildComparableRight(EqMixin, ComparableRight):
    """__ne__ the right way (uses ==)"""

class ChildComparablePy3(EqMixin, BaseEquatable):
    """No __ne__, only right in Python 3."""

Utwórz instancje nie równoważne:

right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)

Spodziewane zachowanie:

(Uwaga: chociaż co drugie stwierdzenie każdego z poniższych jest równoważne, a zatem logicznie nadmiarowe w stosunku do poprzedniego, dołączam je, aby wykazać, że kolejność nie ma znaczenia, gdy jedno jest podklasą drugiego. )

Te wystąpienia zostały __ne__zaimplementowane z ==:

assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1

Te instancje, testowane w Pythonie 3, również działają poprawnie:

assert not right_py3_1 == right_py3_2
assert not right_py3_2 == right_py3_1
assert right_py3_1 != right_py3_2
assert right_py3_2 != right_py3_1

Przypomnijmy, że zostały one __ne__zaimplementowane z __eq__- chociaż jest to oczekiwane zachowanie, implementacja jest nieprawidłowa:

assert not wrong1 == wrong2         # These are contradicted by the
assert not wrong2 == wrong1         # below unexpected behavior!

Nieoczekiwane zachowanie:

Zauważ, że to porównanie jest sprzeczne z porównaniami powyżej ( not wrong1 == wrong2).

>>> assert wrong1 != wrong2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

i,

>>> assert wrong2 != wrong1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

Nie pomijaj __ne__w Pythonie 2

Aby dowiedzieć się, że nie należy pomijać implementacji __ne__w Pythonie 2, zobacz te równoważne obiekty:

>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True

Powyższy wynik powinien być False!

Źródło Pythona 3

Domyślna implementacja CPythona dla __ne__znajduje się typeobject.cwobject_richcompare :

case Py_NE:
    /* By default, __ne__() delegates to __eq__() and inverts the result,
       unless the latter returns NotImplemented. */
    if (Py_TYPE(self)->tp_richcompare == NULL) {
        res = Py_NotImplemented;
        Py_INCREF(res);
        break;
    }
    res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
    if (res != NULL && res != Py_NotImplemented) {
        int ok = PyObject_IsTrue(res);
        Py_DECREF(res);
        if (ok < 0)
            res = NULL;
        else {
            if (ok)
                res = Py_False;
            else
                res = Py_True;
            Py_INCREF(res);
        }
    }
    break;

Ale domyślne __ne__zastosowania __eq__?

Domyślne __ne__szczegóły implementacji Pythona 3 na poziomie C są używane, __eq__ponieważ wyższy poziom ==( PyObject_RichCompare ) byłby mniej wydajny - i dlatego musi również obsługiwać NotImplemented.

Jeśli __eq__jest poprawnie zaimplementowany, negacja ==jest również poprawna - i pozwala nam uniknąć szczegółów implementacji niskiego poziomu w naszym __ne__.

Korzystanie ==pozwala nam zachować logikę niskiego poziomu w jednym miejscu i uniknąć adresowania NotImplementedw __ne__.

Można by błędnie założyć, że ==może powrócić NotImplemented.

W rzeczywistości używa tej samej logiki co domyślna implementacja __eq__, która sprawdza tożsamość (patrz do_richcompare i nasze dowody poniżej)

class Foo:
    def __ne__(self, other):
        return NotImplemented
    __eq__ = __ne__

f = Foo()
f2 = Foo()

I porównania:

>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True

Występ

Nie wierz mi na słowo, zobaczmy, co jest bardziej wydajne:

class CLevel:
    "Use default logic programmed in C"

class HighLevelPython:
    def __ne__(self, other):
        return not self == other

class LowLevelPython:
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

def c_level():
    cl = CLevel()
    return lambda: cl != cl

def high_level_python():
    hlp = HighLevelPython()
    return lambda: hlp != hlp

def low_level_python():
    llp = LowLevelPython()
    return lambda: llp != llp

Myślę, że te liczby mówią same za siebie:

>>> import timeit
>>> min(timeit.repeat(c_level()))
0.09377292497083545
>>> min(timeit.repeat(high_level_python()))
0.2654011140111834
>>> min(timeit.repeat(low_level_python()))
0.3378178110579029

Ma to sens, jeśli weźmiesz pod uwagę, że low_level_pythonw Pythonie jest wykonywana logika, która w innym przypadku byłaby obsługiwana na poziomie C.

Odpowiedź na niektórych krytyków

Inny odpowiadający pisze:

Realizacja Aaron Hall not self == otherz __ne__metody jest błędne, gdyż nigdy nie może wrócić NotImplemented( not NotImplementedjest False), a zatem __ne__metoda, która ma pierwszeństwo nigdy nie może spaść z powrotem na __ne__metody, które nie mają priorytet.

Brak __ne__powrotu NotImplementednie oznacza, że ​​jest to błędne. Zamiast tego obsługujemy priorytetyzację za NotImplementedpomocą sprawdzania równości z ==. Zakładając, że ==zostało poprawnie zaimplementowane, gotowe.

not self == otherbyła to domyślna implementacja __ne__metody w Pythonie 3, ale był to błąd i został poprawiony w Pythonie 3.4 w styczniu 2015 r., jak zauważył ShadowRanger (patrz numer 21408).

Cóż, wyjaśnijmy to.

Jak wspomniano wcześniej, Python 3 domyślnie obsługuje __ne__, najpierw sprawdzając, czy self.__eq__(other)zwraca NotImplemented(singleton) - co powinno być sprawdzane isi zwracane, jeśli tak, w przeciwnym razie powinien zwrócić odwrotność. Oto logika zapisana jako mieszanka klas:

class CStyle__ne__:
    """Mixin that provides __ne__ functionality equivalent to 
    the builtin functionality
    """
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

Jest to konieczne dla poprawności interfejsu API języka Python na poziomie C i zostało wprowadzone w Pythonie 3, tworząc

zbędny. Wszystkie odpowiednie __ne__metody zostały usunięte, w tym te implementujące własne sprawdzenie, a także te, które delegują __eq__bezpośrednio lub za pośrednictwem ==- i ==był to najczęstszy sposób robienia tego.

Czy symetria jest ważna?

Nasz krytyk zapewnia trwałe patologiczną przykład, aby sprawę do postępowania NotImplementedw __ne__ceniąc symetrię ponad wszystko. Stwórzmy argument z jasnym przykładem:

class B:
    """
    this class has no __eq__ implementation, but asserts 
    any instance is not equal to any other object
    """
    def __ne__(self, other):
        return True

class A:
    "This class asserts instances are equivalent to all other objects"
    def __eq__(self, other):
        return True

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, False, True)

Tak więc, zgodnie z tą logiką, aby zachować symetrię, musimy napisać skomplikowaną __ne__, niezależnie od wersji Pythona.

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return True
    def __ne__(self, other):
        result = other.__eq__(self)
        if result is NotImplemented:
            return NotImplemented
        return not result

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, True, True)

Najwyraźniej nie powinniśmy przejmować się tym, że te przypadki są równe i nierówne.

Proponuję, że symetria jest mniej ważna niż domniemanie rozsądnego kodu i przestrzeganie zaleceń dokumentacji.

Gdyby jednak A miał sensowną implementację __eq__, moglibyśmy nadal podążać za moim kierunkiem tutaj i nadal mielibyśmy symetrię:

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return False         # <- this boolean changed... 

>>> A() == B(), B() == A(), A() != B(), B() != A()
(False, False, True, True)

Wniosek

W przypadku kodu zgodnego z Python 2 użyj ==do implementacji __ne__. To jest więcej:

  • poprawny
  • prosty
  • wykonujący

Tylko w Pythonie 3 używaj negacji niskopoziomowej na poziomie C - jest jeszcze prostsza i bardziej wydajna (chociaż to programista jest odpowiedzialny za ustalenie, że jest poprawna ).

Ponownie, nie pisz logiki niskiego poziomu w języku Python wysokiego poziomu.

Aaron Hall
źródło
3
Doskonałe przykłady! Częściowym zaskoczeniem jest to, że kolejność operandów w ogóle nie ma znaczenia , w przeciwieństwie do niektórych magicznych metod z ich odbiciami „po prawej stronie”. Aby powtórzyć tę część, którą przegapiłem (i która kosztowała mnie dużo czasu): najpierw wypróbowywana jest bogata metoda porównania podklasy , niezależnie od tego, czy kod ma nadklasę, czy podklasę po lewej stronie operatora. Jest to dlaczego a1 != c2wrócił False--- nie uruchomić a1.__ne__, ale c2.__ne__, który negował mixin za __eq__ metodę. Ponieważ NotImplementedjest prawdą, not NotImplementedjest False.
Kevin J. Chase
2
Twoje ostatnie aktualizacje z powodzeniem demonstrują przewagę wydajności not (self == other), ale nikt nie twierdzi, że nie jest szybki (no cóż, i tak szybszy niż jakakolwiek inna opcja w Py2). Problem polega na tym, że w niektórych przypadkach jest to złe ; Sam Python robił to kiedyś not (self == other), ale zmienił się, ponieważ był niepoprawny w obecności dowolnych podklas . Odpowiedź od najszybszego do złego jest nadal błędna .
ShadowRanger
1
Ten konkretny przykład jest naprawdę nieistotny. Problem polega na tym, że w twojej implementacji zachowanie twoich __ne__delegatów __eq__(z obu stron, jeśli to konieczne), ale nigdy nie spada __ne__na drugą stronę, nawet jeśli obaj __eq__"poddają się". Prawidłowych __ne__delegatów na swoje własne __eq__ , ale jeśli to powróci NotImplemented, cofa się, aby przejść na drugą stronę __ne__, zamiast odwracać drugą stronę __eq__(ponieważ druga strona mogła nie wyrazić wyraźnej zgody na delegowanie __eq__, a ty nie powinieneś podejmować taką decyzję za to).
ShadowRanger
1
@AaronHall: Po ponownym zbadaniu tego dzisiaj, nie sądzę, aby twoja implementacja była normalnie problematyczna dla podklas (byłoby bardzo zawiłe, gdyby się zepsuła, a podklasa, zakładając, że ma pełną wiedzę o rodzicu, powinna być w stanie tego uniknąć ). Ale w mojej odpowiedzi podałem nieskomplikowany przykład. Niepatologicznym przypadkiem jest ORM SQLAlchemy, w którym ani __eq__ani nie __ne__zwraca albo Truealbo False, ale raczej obiekt proxy (który okazuje się być „prawdziwy”). Nieprawidłowe wdrożenie __ne__oznacza, że ​​dla porównania liczy się kolejność (proxy dostajesz tylko w jednym zamówieniu).
ShadowRanger
1
Aby było jasne, w 99% (a może 99,999%) przypadków rozwiązanie jest w porządku i (oczywiście) szybsze. Ale ponieważ nie masz kontroli nad przypadkami, w których nie jest to w porządku, jako autor biblioteki, którego kod może być używany przez innych (czytaj: wszystko oprócz prostych, jednorazowych skryptów i modułów wyłącznie do użytku osobistego), musisz używaj poprawnej implementacji, aby przestrzegać umowy ogólnej dotyczącej przeciążania operatora i pracuj z dowolnym innym kodem, który możesz napotkać. Na szczęście na Py3 nic z tego nie ma znaczenia, ponieważ możesz __ne__całkowicie pominąć . Za rok Py2 będzie martwy i ignorujemy to. :-)
ShadowRanger
10

Tak dla przypomnienia, kanonicznie poprawny i krzyżowy przenośny Py2 / Py3 __ne__wyglądałby tak:

import sys

class ...:
    ...
    def __eq__(self, other):
        ...

    if sys.version_info[0] == 2:
        def __ne__(self, other):
            equal = self.__eq__(other)
            return equal if equal is NotImplemented else not equal

Działa to z każdym, __eq__który możesz zdefiniować:

  • W przeciwieństwie do tego not (self == other), nie koliduje z niektórymi irytującymi / złożonymi przypadkami obejmującymi porównania, w których jedna z klas nie oznacza, że ​​wynik __ne__jest taki sam jak wynik noton __eq__(np. ORM SQLAlchemy, gdzie oba __eq__i __ne__zwracają specjalne obiekty proxy, nie Truelub False, a próba zwrócenia notwyniku __eq__zwróci Falsezamiast prawidłowego obiektu proxy).
  • W przeciwieństwie not self.__eq__(other), to poprawnie delegatów na __ne__od drugiej instancji, gdy self.__eq__powraca NotImplemented( not self.__eq__(other)byłoby extra źle, bo NotImplementedjest truthy, więc gdy __eq__nie wiedział, jak przeprowadzić porównanie, __ne__by powrócić False, co oznacza, że dwa obiekty są równe, gdy w rzeczywistości jedynym Obiekt zapytany nie miał pojęcia, co oznaczałoby brak równości)

Jeśli __eq__nie używasz NotImplementedzwrotów, to działa (z bezsensownym narzutem), jeśli NotImplementedczasami używa , to obsługuje to poprawnie. A sprawdzenie wersji Pythona oznacza, że ​​jeśli klasa jest import-ed w Pythonie 3, __ne__pozostaje niezdefiniowana, co pozwala na przejęcie natywnej, wydajnej __ne__implementacji rezerwowej Pythona (wersja C powyższego) .


Dlaczego jest to potrzebne

Reguły przeciążania Pythona

Wyjaśnienie, dlaczego robisz to zamiast innych rozwiązań, jest nieco tajemnicze. Python ma kilka ogólnych zasad dotyczących przeciążania operatorów, w szczególności operatorów porównania:

  1. (Dotyczy wszystkich operatorów) Podczas uruchamiania LHS OP RHSspróbuj LHS.__op__(RHS), a jeśli to zwróci NotImplemented, spróbuj RHS.__rop__(LHS). Wyjątek: jeśli RHSjest to podklasa LHSklasy 's, RHS.__rop__(LHS) najpierw przetestuj . W przypadku operatorów porównania, __eq__a __ne__są ich własne „RPO” s (tak kolejność test na __ne__to LHS.__ne__(RHS), wtedy RHS.__ne__(LHS), odwracane, jeśli RHSjest podklasą LHS„s klasy)
  2. Oprócz idei operatora „zamiany”, nie ma domniemanego związku między operatorami. Nawet w przypadku tej samej klasy LHS.__eq__(RHS)zwracanie Truenie oznacza LHS.__ne__(RHS)zwrotów False(w rzeczywistości operatory nie są nawet zobowiązane do zwracania wartości logicznych; ORMy, takie jak SQLAlchemy, celowo tego nie robią, pozwalając na bardziej wyrazistą składnię zapytań). Od Pythona 3 domyślna __ne__implementacja zachowuje się w ten sposób, ale nie jest to umowne; możesz nadpisać __ne__w sposób, który nie jest ścisłym przeciwieństwem __eq__.

Jak to się ma do przeciążania komparatorów

Więc kiedy przeciążasz operatora, masz dwie prace:

  1. Jeśli wiesz, jak samodzielnie zaimplementować operację, zrób to, wykorzystując tylko własną wiedzę o tym, jak wykonać porównanie (nigdy nie deleguj, w sposób dorozumiany lub jawny, na drugą stronę operacji; robienie tego grozi niepoprawnością i / lub nieskończoną rekurencją, w zależności od tego, jak to zrobisz)
  2. Jeśli nie wiesz, jak zaimplementować operację samodzielnie, zawsze zwracaj NotImplemented, aby Python mógł delegować implementację drugiego operandu

Problem z not self.__eq__(other)

def __ne__(self, other):
    return not self.__eq__(other)

nigdy nie deleguje na drugą stronę (i jest niepoprawna, jeśli __eq__poprawnie zwraca NotImplemented). Kiedy self.__eq__(other)zwraca NotImplemented(co jest „prawdą”), po cichu wracasz False, więc A() != something_A_knows_nothing_aboutwraca False, gdy powinien był sprawdzić, czy something_A_knows_nothing_aboutwie, jak porównać z wystąpieniami A, a jeśli nie, powinien był wrócić True(ponieważ jeśli żadna ze stron nie wie jak w porównaniu z innymi, nie są sobie równe). Jeśli A.__eq__jest niepoprawnie zaimplementowane (zwraca Falsezamiast, NotImplementedgdy nie rozpoznaje drugiej strony), to jest to „poprawne” z Aperspektywy, zwracające True(ponieważ Anie uważa, że ​​jest równe, więc nie jest równe), ale może być źle odsomething_A_knows_nothing_aboutperspektywy, ponieważ nigdy nie pytał something_A_knows_nothing_about; A() != something_A_knows_nothing_aboutkończy się True, ale something_A_knows_nothing_about != A()może False, lub jakakolwiek inna wartość zwracana.

Problem z not self == other

def __ne__(self, other):
    return not self == other

jest bardziej subtelny. Będzie to poprawne dla 99% klas, w tym wszystkich klas, dla których __ne__jest logiczną odwrotnością __eq__. Ale not self == otherłamie obie powyższe reguły, co oznacza, że ​​dla klas, dla których __ne__ nie jest logiczną odwrotnością __eq__, wyniki są ponownie niesymetryczne, ponieważ jeden z operandów nigdy nie jest pytany, czy w ogóle może zaimplementować __ne__, nawet jeśli drugi operand nie może. Najprostszym przykładem jest klasa dziwakiem, który powraca Falsedo wszystkich porównań, więc A() == Incomparable()i A() != Incomparable()obaj powrót False. Przy poprawnej implementacji A.__ne__(takiej, która zwraca, NotImplementedgdy nie wie, jak zrobić porównanie), relacja jest symetryczna; A() != Incomparable()iIncomparable() != A()uzgodnić wynik (bo w pierwszym przypadku A.__ne__wraca NotImplemented, potem Incomparable.__ne__wraca False, aw drugim Incomparable.__ne__zwraca Falsebezpośrednio). Ale kiedy A.__ne__jest implementowane jako return not self == other, A() != Incomparable()zwraca True(ponieważ A.__eq__zwraca, nie NotImplemented, potem Incomparable.__eq__zwraca Falsei A.__ne__odwraca to do True), podczas gdy Incomparable() != A()zwracaFalse.

Możesz zobaczyć przykład tego w akcji tutaj .

Oczywiście klasa, która zawsze wraca Falsedla obu __eq__i __ne__jest trochę dziwna. Ale jak wspomniano wcześniej, __eq__i __ne__nawet nie trzeba zwracać True/ False; SQLAlchemy ORM ma klasy z komparatorami, które zwracają specjalny obiekt proxy do budowania zapytań, wcale True/ Falsewcale (są „prawdziwe”, jeśli są oceniane w kontekście logicznym, ale nigdy nie powinny być oceniane w takim kontekście).

Poprzez brak przeciążenia __ne__prawidłowo, to będzie przerwa klas tego rodzaju, jak kod:

 results = session.query(MyTable).filter(MyTable.fieldname != MyClassWithBadNE())

zadziała (zakładając, że SQLAlchemy w ogóle wie, jak wstawić MyClassWithBadNEdo ciągu SQL; można to zrobić za pomocą adapterów typów bez MyClassWithBadNEkonieczności jakiejkolwiek współpracy), przekazując oczekiwany obiekt proxy do filter, podczas gdy:

 results = session.query(MyTable).filter(MyClassWithBadNE() != MyTable.fieldname)

zakończy się przekazaniem filterzwykłego False, ponieważ self == otherzwraca obiekt proxy i not self == otherpo prostu konwertuje prawdziwy obiekt proxy na False. Miejmy nadzieję, że filterzgłasza wyjątek w przypadku obsługi nieprawidłowych argumentów, takich jak False. Chociaż jestem pewien, że wielu będzie argumentować, że MyTable.fieldname powinno to być konsekwentnie po lewej stronie porównania, faktem jest, że nie ma programowego powodu, aby wymuszać to w ogólnym przypadku, a poprawny rodzaj generyczny __ne__będzie działał tak czy inaczej, podczas gdy return not self == otherdziała tylko w jednym układzie.

ShadowRanger
źródło
1
Jedyna poprawna, kompletna i uczciwa (przepraszam @AaronHall) odpowiedź. To powinna być akceptowana odpowiedź.
Maggyero
Możesz być zainteresowany moją zaktualizowanej odpowiedzi, która wykorzystuje myślę silniejszego argumentu niż Incomparableklasy od tej klasie łamie dopełniacza relacji pomiędzy !=i ==operatorów, a zatem może być uznane za nieważne lub „patologiczne” przykład jak @AaronHall umieścić go. I przyznaję, że @AaronHall ma rację, kiedy wskazał, że twój argument SQLAlchemy może zostać uznany za nieistotny, ponieważ jest w kontekście innym niż boolowski. (Twoje argumenty są nadal bardzo interesujące i przemyślane.)
Maggyero
4

Prawidłowa __ne__realizacja

Implementacja metody specjalnej @ ShadowRanger __ne__jest poprawna:

def __ne__(self, other):
    result = self.__eq__(other)
    if result is not NotImplemented:
        return not result
    return NotImplemented

Tak się składa, że ​​jest to również domyślna implementacja metody specjalnej __ne__ od czasu Pythona 3.4 , jak podano w dokumentacji Pythona :

Domyślnie __ne__()deleguje __eq__()i odwraca wynik, chyba że tak jest NotImplemented.

Należy również zauważyć, że zwracanie wartości NotImplementednieobsługiwanych operandów nie jest specyficzne dla metody specjalnej __ne__. W rzeczywistości wszystkie specjalne metody porównania 1 i specjalne metody numeryczne 2 powinny zwracać wartość NotImplementeddla nieobsługiwanych operandów , jak określono w dokumentacji Pythona :

Nie zaimplementowano

Ten typ ma jedną wartość. Istnieje jeden obiekt o tej wartości. Dostęp do tego obiektu uzyskuje się za pośrednictwem wbudowanej nazwy NotImplemented. Metody numeryczne i bogate metody porównania powinny zwracać tę wartość, jeśli nie implementują operacji dla podanych operandów. (W zależności od operatora interpreter spróbuje wykonać odzwierciedloną operację lub inną rezerwę). Jego wartość prawda to prawda.

Przykład specjalnych metod numerycznych znajduje się w dokumentacji Pythona :

class MyIntegral(Integral):

    def __add__(self, other):
        if isinstance(other, MyIntegral):
            return do_my_adding_stuff(self, other)
        elif isinstance(other, OtherTypeIKnowAbout):
            return do_my_other_adding_stuff(self, other)
        else:
            return NotImplemented

    def __radd__(self, other):
        if isinstance(other, MyIntegral):
            return do_my_adding_stuff(other, self)
        elif isinstance(other, OtherTypeIKnowAbout):
            return do_my_other_adding_stuff(other, self)
        elif isinstance(other, Integral):
            return int(other) + int(self)
        elif isinstance(other, Real):
            return float(other) + float(self)
        elif isinstance(other, Complex):
            return complex(other) + complex(self)
        else:
            return NotImplemented

1 Specjalne metody porównania: __lt__, __le__, __eq__, __ne__, __gt__i __ge__.

2 specjalne metody numerycznej __add__, __sub__, __mul__, __matmul__, __truediv__, __floordiv__, __mod__, __divmod__, __pow__, __lshift__, __rshift__, __and__,__xor__ , __or__i ich __r*__odbicie i __i*__odpowiedników w miejscu.

Nieprawidłowa __ne__implementacja # 1

Implementacja metody specjalnej @ Falmarri __ne__jest nieprawidłowa:

def __ne__(self, other):
    return not self.__eq__(other)

Problem z tą implementacją polega na tym, że nie opiera się ona na specjalnej metodzie __ne__innego operandu, ponieważ nigdy nie zwraca wartości NotImplemented(wyrażenie not self.__eq__(other)oblicza wartość Truelub False, w tym gdy jego podwyrażenie szacuje self.__eq__(other)wartość, NotImplementedponieważ wyrażenie bool(NotImplemented)oblicza wartość True). Boolowska ocena wartości NotImplementedprzerywa zależność dopełniania między operatorami porównania !=i ==:

class Correct:

    def __ne__(self, other):
        result = self.__eq__(other)
        if result is not NotImplemented:
            return not result
        return NotImplemented


class Incorrect:

    def __ne__(self, other):
        return not self.__eq__(other)


x, y = Correct(), Correct()
assert (x != y) is not (x == y)

x, y = Incorrect(), Incorrect()
assert (x != y) is not (x == y)  # AssertionError

Błędny __ne__ implementacja # 2

Implementacja metody specjalnej @ AaronHall __ne__jest również niepoprawna:

def __ne__(self, other):
    return not self == other

Problem z tą implementacją polega na tym, że bezpośrednio powraca do specjalnej metody __eq__innego operandu, pomijając specjalną metodę __ne__innego operandu, ponieważ nigdy nie zwraca wartości NotImplemented(wyrażenie not self == otherpowraca do specjalnej metody __eq__innego operandu i zwraca wartość Truelub False). Pomijanie metody jest niepoprawne, ponieważ ta metoda może mieć skutki uboczne, takie jak aktualizowanie stanu obiektu:

class Correct:

    def __init__(self):
        self.counter = 0

    def __ne__(self, other):
        self.counter += 1
        result = self.__eq__(other)
        if result is not NotImplemented:
            return not result
        return NotImplemented


class Incorrect:

    def __init__(self):
        self.counter = 0

    def __ne__(self, other):
        self.counter += 1
        return not self == other


x, y = Correct(), Correct()
assert x != y
assert x.counter == y.counter

x, y = Incorrect(), Incorrect()
assert x != y
assert x.counter == y.counter  # AssertionError

Zrozumienie operacji porównawczych

W matematyce relacja binarna R nad zbiorem X jest zbiorem uporządkowanych par ( xy ) w  X 2 . Instrukcja ( xy ) w  R brzmi „ x jest R- powiązane z y ” i jest oznaczona przez xRy .

Własności relacji binarnej R na zbiorze X :

  • R jest zwrotne, gdy dla wszystkich x w X , xRx .
  • R jest nierefleksyjne (zwane również ścisłym ), gdy dla wszystkich x w X , a nie xRx .
  • R jest symetryczny gdy wszystkie X i Y w X , jeśli xRy następnie yRx .
  • R jest antysymetryczna , gdy dla wszystkich X i Y w X , jeśli xRy i yRx czym x  =  y .
  • R jest przechodnia , gdy dla wszystkich x , Y i Z w X , jeśli xRy i yRz następnie xRz .
  • R jest connex (zwany również całkowity ), gdy dla wszystkich X i Y w X , xRy lub yRx .
  • R jest relacją równoważności, gdy R jest zwrotne, symetryczne i przechodnie.
    Na przykład =. Jednak ≠ jest tylko symetryczne.
  • R jest relacją porządku, gdy R jest zwrotne, antysymetryczne i przechodnie.
    Na przykład ≤ i ≥.
  • R jest relacją ścisłego porządku, gdy R jest nierefleksyjna, antysymetryczna i przechodnia.
    Na przykład <i>. Jednak ≠ jest tylko nierefleksyjne.

Operacje na dwóch relacjach binarnych R i S na zbiorze X :

  • Rozmawiać z R jest binarny stosunek R T  = {( yx ) | xRy } nad X .
  • Dopełniacza z R jest binarny związek Ź R  = {( xy ) | Nie xRy } nad X .
  • Związek z R i S jest binarny stosunek R  ∪  S  = {( xy ) | xRy lub xSy } nad X .

Relacje między relacjami porównawczymi, które są zawsze aktualne:

  • 2 komplementarne relacje: = i ≠ uzupełniają się nawzajem;
  • 6 relacji odwrotnych: = jest odwrotnością samego siebie, ≠ jest odwrotnością samego siebie, <i> są wzajemnymi przeciwieństwami, a ≤ i ≥ są nawzajem odwrotnymi;
  • 2 relacje związkowe: ≤ jest związkiem <i =, a ≥ jest związkiem> i =.

Relacje między relacjami porównawczymi, które są ważne tylko dla zamówień connex :

  • 4 uzupełniające się relacje: <i ≥ są dopełnieniem siebie nawzajem, a> i ≤ ​​są dopełnieniem siebie nawzajem.

Tak, aby prawidłowo realizować w Pythonie operatory porównania ==, !=, <, >, <=, i>= odpowiadającej relacji porównania =, ≠, <,>, ≤, ≥ i wszystkie powyższe właściwości matematyczne i relacje powinny utrzymać.

Operacja porównania x operator ywywołuje specjalną metodę porównania __operator__klasy jednego z jej operandów:

class X:

    def __operator__(self, other):
        # implementation

Ponieważ R jest zwrotna oznacza XRX , refleksyjnym operacja porównania x operator y( x == y, x <= yi x >= y) lub refleksyjne specjalny wywołanie metody porównania x.__operator__(y)( x.__eq__(y), x.__le__(y)i x.__ge__(y)) należy oceniać wartości True, jeśli xi ysą identyczne, to znaczy, jeśli wyrażenie x is yma wartość True. Ponieważ R jest irreflexive oznacza nie XRX , irreflexive operacja porównania x operator y( x != y, x < yi x > y) lub irreflexive wywołanie specjalnej metody porównania x.__operator__(y)( x.__ne__(y), x.__lt__(y)ax.__gt__(y) ) należy oceniać wartościFalsejeśli xi ysą identyczne, to znaczy, jeśli x is ywynikiem wyrażenia jest True. Nieruchomość odruchowy jest uważany przez Pythonie dla operatora porównania ==i związany specjalną metodę porównawczą __eq__, ale zaskakująco nie uznane za operatorów porównania <=i >=i związany specjalnych metod porównawczych __le__i __ge__, a właściwość irreflexive jest uważany przez Pythonie dla operatora porównania !=i związany specjalną metodę porównawczą __ne__, ale zaskakująco nie uznane za operatorów porównania <i >i związany specjalnych metod porównawczych __lt__i__gt__. Zignorowane operatory porównania zamiast tego zgłaszają wyjątekTypeError(i powiązane specjalne metody porównawcze zamiast tego zwracają wartość NotImplemented), jak wyjaśniono w dokumentacji Pythona :

Domyślne zachowanie dla porównania równości ( ==i !=) jest oparte na tożsamości obiektów. W związku z tym porównanie równości instancji o tej samej tożsamości skutkuje równością, a porównanie równości instancji o różnych tożsamościach skutkuje nierównością. Motywacją do tego domyślnego zachowania jest pragnienie, aby wszystkie przedmioty były refleksyjne (tj. x is ySugerowały x == y).

Porównanie kolejność domyślna ( <, >, <=, i >=) nie jest przewidziane; pojawia się próba TypeError. Motywacją do tego domyślnego zachowania jest brak podobnego niezmiennika jak w przypadku równości. [Jest to nieprawidłowe, ponieważ <=i >=są refleksyjne, jak ==i <a >są irreflexive podobne !=].

Klasa objectzapewnia domyślne implementacje specjalnych metod porównania, które są dziedziczone przez wszystkie jej podklasy, jak wyjaśniono w dokumentacji Pythona :

object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)

Są to tak zwane metody „bogatych porównań”. Zależność między symbolami operatorów a nazwami metod jest następująca: x<ypołączenia x.__lt__(y), x<=ypołączenia x.__le__(y), x==ypołączenia x.__eq__(y), x!=ypołączenia x.__ne__(y), x>ypołączenia x.__gt__(y)i x>=y połączenia x.__ge__(y).

Bogata metoda porównania może zwrócić singleton, NotImplementedjeśli nie implementuje operacji dla danej pary argumentów.

[…]

Nie ma wersji tych metod z zamienionymi argumentami (do użycia, gdy lewy argument nie obsługuje operacji, ale prawy argument tak); raczej, __lt__()i __gt__()są odbiciem siebie nawzajem, __le__()i __ge__()to odbicie siebie, a __eq__()i __ne__()są ich własnym odbiciem. Jeśli operandy są różnych typów, a typ prawego operandu jest bezpośrednią lub pośrednią podklasą typu lewego operandu, metoda odbita prawego operandu ma priorytet, w przeciwnym razie pierwszeństwo ma metoda lewego operandu. Wirtualne podklasy nie są brane pod uwagę.

Od R = ( R T ) T , porównanie xRy jest równoważna odwrotnego stosunku yr T x (nieformalnie nazwie "odbicie" w dokumentacji Python). Istnieją więc dwa sposoby obliczenia wyniku operacji porównania x operator y: wywołanie albo x.__operator__(y)lub y.__operatorT__(x). Python używa następującej strategii obliczeniowej:

  1. Wywołuje, x.__operator__(y)chyba że klasa prawego operandu jest potomkiem klasy lewego operandu, w którym to przypadku wywołuje y.__operatorT__(x)( pozwalając klasom na przesłonięcie odwrotnej specjalnej metody porównania ich przodków ).
  2. Jeśli operandy xi ynie są obsługiwane (wskazywane przez wartość zwracaną NotImplemented), wywołuje specjalną metodę porównania converse jako pierwszą rezerwę .
  3. Jeśli operandy xi ysą nieobsługiwane (wskazywane przez wartość zwracaną NotImplemented), zgłasza wyjątek TypeErrorz wyjątkiem operatorów porównania ==i !=dla których testuje odpowiednio tożsamość i nie-tożsamość operandów xoraz yjako drugą rezerwę (wykorzystując właściwość zwrotności ==i właściwość nieodwracalności !=).
  4. Zwraca wynik.

W CPython ten wprowadzany jest kod C , który może ulegać translacji do Pythonie (nazwami eqdla ==, nena !=, lto <, gto >, leo <=, a gew >=)

def eq(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__eq__(left)
        if result is NotImplemented:
            result = left.__eq__(right)
    else:
        result = left.__eq__(right)
        if result is NotImplemented:
            result = right.__eq__(left)
    if result is NotImplemented:
        result = left is right
    return result
def ne(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ne__(left)
        if result is NotImplemented:
            result = left.__ne__(right)
    else:
        result = left.__ne__(right)
        if result is NotImplemented:
            result = right.__ne__(left)
    if result is NotImplemented:
        result = left is not right
    return result
def lt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__gt__(left)
        if result is NotImplemented:
            result = left.__lt__(right)
    else:
        result = left.__lt__(right)
        if result is NotImplemented:
            result = right.__gt__(left)
    if result is NotImplemented:
        raise TypeError(
            f"'<' not supported between instances of '{type(left).__name__}' "
            f"and '{type(right).__name__}'"
        )
    return result
def gt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__lt__(left)
        if result is NotImplemented:
            result = left.__gt__(right)
    else:
        result = left.__gt__(right)
        if result is NotImplemented:
            result = right.__lt__(left)
    if result is NotImplemented:
        raise TypeError(
            f"'>' not supported between instances of '{type(left).__name__}' "
            f"and '{type(right).__name__}'"
        )
    return result
def le(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ge__(left)
        if result is NotImplemented:
            result = left.__le__(right)
    else:
        result = left.__le__(right)
        if result is NotImplemented:
            result = right.__ge__(left)
    if result is NotImplemented:
        raise TypeError(
            f"'<=' not supported between instances of '{type(left).__name__}' "
            f"and '{type(right).__name__}'"
        )
    return result
def ge(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__le__(left)
        if result is NotImplemented:
            result = left.__ge__(right)
    else:
        result = left.__ge__(right)
        if result is NotImplemented:
            result = right.__le__(left)
    if result is NotImplemented:
        raise TypeError(
            f"'>=' not supported between instances of '{type(left).__name__}' "
            f"and '{type(right).__name__}'"
        )
    return result

Ponieważ R = ¬ (¬ R ), porównanie xRy jest równoważne porównaniu dopełniacza ¬ ( x ¬ Ry ). ≠ jest uzupełnieniem =, więc metoda specjalna __ne__jest implementowana __eq__domyślnie w postaci specjalnej metody dla obsługiwanych operandów, podczas gdy inne specjalne metody porównania są domyślnie implementowane niezależnie (fakt, że ≤ jest sumą <i = oraz ≥ jest sumą> i = jest zaskakująco nieuwzględniona , co oznacza, że ​​obecnie specjalne metody __le__i __ge__powinny być zaimplementowane przez użytkownika), jak wyjaśniono w dokumentacji Pythona :

Domyślnie __ne__()deleguje __eq__()i odwraca wynik, chyba że tak jest NotImplemented. Nie ma innych domniemanych relacji między operatorami porównania, na przykład prawda (x<y or x==y)nie implikuje x<=y.

W CPythonie jest to zaimplementowane w kodzie C , który można przetłumaczyć na kod Pythona:

def __eq__(self, other):
    return self is other or NotImplemented
def __ne__(self, other):
    result = self.__eq__(other)
    if result is not NotImplemented:
        return not result
    return NotImplemented
def __lt__(self, other):
    return NotImplemented
def __gt__(self, other):
    return NotImplemented
def __le__(self, other):
    return NotImplemented
def __ge__(self, other):
    return NotImplemented

Więc domyślnie:

  • operacja porównania x operator ypodnosi wyjątek TypeErrorz wyjątkiem operatorów porównania ==i !=do którego powraca odpowiednio tożsamości i braku tożsamości argumentów xi y;
  • specjalne wywołanie metody porównania x.__operator__(y)zwraca wartość NotImplementedz wyjątkiem specjalnych metod porównawczych __eq__i __ne__dla których zwraca odpowiednio Truei Falsejeśli operandy xi ysą odpowiednio identyczne i nieidentyczne, a wartość w NotImplementedprzeciwnym razie.
Maggyero
źródło
Ostatni przykład: „Ponieważ ta implementacja nie powiela zachowania domyślnej implementacji __ne__metody, gdy __eq__metoda zwraca NotImplemented, jest ona niepoprawna”. - Adefiniuje bezwarunkową równość. Zatem A() == B(). Zatem A() != B() powinno być fałszem i tak jest . Podane przykłady są patologiczne (tj. __ne__Nie powinny zwracać łańcucha i __eq__nie powinny zależeć __ne__- raczej __ne__powinny zależeć od __eq__, co jest domyślnym oczekiwaniem w Pythonie 3). Nadal jestem -1 w tej odpowiedzi, dopóki nie zmienisz zdania.
Aaron Hall
@AaronHall Z dokumentacji języka Python : „Bogata metoda porównania może zwrócić singleton, NotImplementedjeśli nie implementuje operacji dla danej pary argumentów. Zgodnie z konwencją Falsei Truesą zwracane w celu pomyślnego porównania. Jednak metody te mogą zwracać dowolną wartość , więc jeśli operator porównania jest używany w kontekście boolowskim (np. w warunku instrukcji if), Python wywoła bool()wartość, aby określić, czy wynik jest prawdziwy, czy fałszywy. "
Maggyero
Ostatni przykład ma dwie klasy, Bktóre zwracają prawdziwy ciąg znaków przy wszystkich sprawdzeniach __ne__i Azwracają Trueprzy wszystkich sprawdzeniach __eq__. To jest patologiczna sprzeczność. W przypadku takiej sprzeczności najlepiej byłoby zgłosić wyjątek. Bez wiedzy B, Anie ma obowiązku przestrzegania B„s wdrażania __ne__dla celów symetrii. W tym momencie w przykładzie nie ma dla mnie znaczenia sposób działania Anarzędzi __ne__. Znajdź praktyczny, niepatologiczny przypadek, aby przedstawić swój punkt widzenia. Zaktualizowałem swoją odpowiedź, aby się do Ciebie zwrócić.
Aaron Hall
Przypadek użycia SQLAlchemy dotyczy języka specyficznego dla domeny. Jeśli ktoś projektuje taki DSL, może wyrzucić przez okno wszystkie rady. Aby dalej torturować tę kiepską analogię, twój przykład zakłada, że ​​samolot będzie leciał do tyłu przez połowę czasu, a mój oczekuje, że lecą tylko do przodu, i myślę, że to rozsądna decyzja projektowa. Uważam, że zgłaszane przez Państwa obawy są nieuzasadnione i zacofane.
Aaron Hall
-1

Jeśli to wszystko __eq__, __ne__, __lt__, __ge__, __le__, i __gt__sensu dla tej klasy, a potem po prostu wdrożyć __cmp__zamiast. W przeciwnym razie rób to, co robisz, z powodu fragmentu, który powiedział Daniel DiPaolo (podczas gdy ja to testowałem zamiast sprawdzać;))

Karl Knechtel
źródło
12
__cmp__()Specjalna metoda nie jest już obsługiwana w Pythonie 3.x więc powinniśmy się przyzwyczaić do korzystania z bogatych operatorów porównania.
Don O'Donnell
8
Lub alternatywnie, jeśli jesteś w Pythonie 2.7 lub 3.x, dekorator functools.total_ordering jest również bardzo przydatny.
Adam Parkin,
Dzięki za ostrzeżenie. Jednak w ciągu ostatniego półtora roku zdałem sobie sprawę z wielu podobnych rzeczy. ;)
Karl Knechtel