Python, sollte ich den Operator __ne __ () basierend auf __eq__ implementieren?

98

Ich habe eine Klasse, in der ich den __eq__()Operator überschreiben möchte . Es scheint sinnvoll zu sein , dass ich das außer Kraft setzen sollte __ne__()auch Betreiber, aber macht es Sinn, zu implementieren , __ne__auf der Grundlage __eq__als solche?

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

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

Oder fehlt mir etwas an der Art und Weise, wie Python diese Operatoren verwendet, was dies nicht zu einer guten Idee macht?

Falmarri
quelle

Antworten:

57

Ja, das ist vollkommen in Ordnung. In der Dokumentation werden Sie aufgefordert, __ne__Folgendes zu definieren __eq__:

Es gibt keine impliziten Beziehungen zwischen den Vergleichsoperatoren. Die Wahrheit von x==ybedeutet nicht, dass dies x!=y falsch ist. Dementsprechend sollte beim Definieren __eq__()auch definiert werden __ne__(), dass sich die Operatoren wie erwartet verhalten.

In vielen Fällen (wie diesem) ist es so einfach wie das Negieren des Ergebnisses von __eq__, aber nicht immer.

Daniel DiPaolo
quelle
12
Dies ist die richtige Antwort (hier unten von @ aaron-hall). Die Dokumentation , die Sie zitiert wird nicht empfehlen Ihnen , zu implementieren , __ne__verwenden __eq__, nur , dass Sie es implementieren.
Guyarad
2
@guyarad: Eigentlich ist Aarons Antwort immer noch etwas falsch, da sie nicht richtig delegiert wurde. Anstatt eine NotImplementedRückgabe von einer Seite als Hinweis für die Delegierung __ne__auf der anderen Seite zu behandeln, not self == otherwird __eq__implizit (vorausgesetzt, der Operand weiß nicht, wie der andere Operand verglichen werden soll) __eq__von der anderen Seite an delegiert und dann invertiert. Bei seltsamen Typen, z. B. den Feldern des SQLAlchemy ORM, verursacht dies Probleme .
ShadowRanger
1
Die Kritik von ShadowRanger würde nur für sehr pathologische Fälle (IMHO) gelten und wird in meiner Antwort unten ausführlich behandelt.
Aaron Hall
1
Die neueren Dokumentationen (zumindest für 3.7 möglicherweise sogar früher) werden __ne__automatisch an delegiert, __eq__und das Zitat in dieser Antwort ist in den Dokumenten nicht mehr vorhanden. Unterm Strich ist es vollkommen pythonisch, nur zu implementieren __eq__und __ne__delegieren zu lassen .
Bluesummers
131

Python, sollte ich einen __ne__()Operator basierend auf implementieren __eq__?

Kurze Antwort: Implementieren Sie es nicht, aber wenn Sie müssen, verwenden Sie es ==nicht__eq__

In Python 3 !=ist die Negation ==standardmäßig so, dass Sie nicht einmal eine schreiben müssen __ne__, und die Dokumentation enthält keine Meinung mehr zum Schreiben einer.

Im Allgemeinen schreiben Sie für Nur-Python-3-Code keinen, es sei denn, Sie müssen die übergeordnete Implementierung überschatten, z. B. für ein integriertes Objekt.

Denken Sie also an den Kommentar von Raymond Hettinger :

Die __ne__Methode folgt automatisch __eq__nur dann, wenn sie __ne__noch nicht in einer Oberklasse definiert ist. Wenn Sie also von einem integrierten System erben, sollten Sie beide überschreiben.

Wenn Sie Ihren Code benötigen, um in Python 2 zu funktionieren, befolgen Sie die Empfehlung für Python 2 und es funktioniert in Python 3 einwandfrei.

In Python 2 implementiert Python selbst keine Operation automatisch in Bezug auf eine andere - daher sollten Sie die __ne__in Bezug auf ==anstelle von definieren __eq__. Z.B

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

Siehe Beweis dafür

  • Implementierungsoperator __ne__()basierend auf __eq__und
  • überhaupt nicht __ne__in Python 2 implementiert

bietet in der folgenden Demonstration ein falsches Verhalten.

Lange Antwort

Die Dokumentation zu Python 2 lautet:

Es gibt keine impliziten Beziehungen zwischen den Vergleichsoperatoren. Die Wahrheit von x==ybedeutet nicht, dass dies x!=yfalsch ist. Dementsprechend sollte beim Definieren __eq__()auch definiert werden __ne__(), dass sich die Operatoren wie erwartet verhalten.

Das heißt, wenn wir __ne__das Gegenteil von definieren __eq__, können wir ein konsistentes Verhalten erhalten.

Dieser Abschnitt der Dokumentation wurde für Python 3 aktualisiert :

Standardmäßig __ne__()delegiert __eq__()und invertiert das Ergebnis, sofern dies nicht der Fall ist NotImplemented.

und im Abschnitt "Was ist neu" sehen wir, dass sich dieses Verhalten geändert hat:

  • !=gibt jetzt das Gegenteil von zurück ==, es sei denn, es wird ==zurückgegeben NotImplemented.

Für die Implementierung __ne__bevorzugen wir die Verwendung des ==Operators anstelle der __eq__direkten Verwendung der Methode. Wenn self.__eq__(other)eine Unterklasse NotImplementedfür den überprüften Typ zurückgibt , prüft Python dies in other.__eq__(self) der Dokumentation entsprechend :

Das NotImplementedObjekt

Dieser Typ hat einen einzelnen Wert. Es gibt ein einzelnes Objekt mit diesem Wert. Auf dieses Objekt wird über den integrierten Namen zugegriffen NotImplemented. Numerische Methoden und umfangreiche Vergleichsmethoden können diesen Wert zurückgeben, wenn sie die Operation für die angegebenen Operanden nicht implementieren. (Der Interpreter versucht dann die reflektierte Operation oder einen anderen Fallback, abhängig vom Operator.) Sein Wahrheitswert ist wahr.

Wenn ein reichen Vergleichsoperator gegeben, wenn sie nicht vom gleichen Typ sind, dann prüft Python , wenn das otherist ein Subtyp, und wenn es , dass die Betreiber definiert hat, verwendet er die other‚s Methode zuerst (inverse für <, <=, >=und >). Wenn NotImplementedzurückgegeben wird , dann verwendet er die Methode des Gegenteils. (Es wird nicht zweimal nach derselben Methode gesucht.) Die Verwendung des ==Operators ermöglicht diese Logik.


Erwartungen

Semantisch sollten Sie __ne__im Hinblick auf die Prüfung auf Gleichheit implementieren, da Benutzer Ihrer Klasse erwarten, dass die folgenden Funktionen für alle Instanzen von A gleichwertig sind:

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

Das heißt, beide oben genannten Funktionen sollten immer das gleiche Ergebnis zurückgeben. Dies hängt jedoch vom Programmierer ab.

Demonstration unerwarteten Verhaltens bei der Definition __ne__basierend auf __eq__:

Zuerst das Setup:

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."""

Nicht äquivalente Instanzen instanziieren:

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

Erwartetes Verhalten:

(Hinweis: Während jede zweite Behauptung der folgenden Aussagen äquivalent und daher logisch redundant zu der vorherigen ist, füge ich sie hinzu, um zu demonstrieren, dass die Reihenfolge keine Rolle spielt, wenn eine eine Unterklasse der anderen ist. )

Diese Instanzen wurden __ne__implementiert mit ==:

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

Diese Instanzen, die unter Python 3 getestet werden, funktionieren ebenfalls ordnungsgemäß:

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

Und denken Sie daran, dass diese __ne__implementiert wurden mit __eq__- während dies das erwartete Verhalten ist, ist die Implementierung falsch:

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

Unerwartetes Verhalten:

Beachten Sie, dass dieser Vergleich den obigen Vergleichen widerspricht ( not wrong1 == wrong2).

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

und,

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

Überspringen Sie nicht __ne__in Python 2

Hinweise, dass Sie die Implementierung __ne__in Python 2 nicht überspringen sollten, finden Sie in den entsprechenden Objekten:

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

Das obige Ergebnis sollte sein False!

Python 3-Quelle

Die Standard-CPython-Implementierung für __ne__ist typeobject.cinobject_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;

Aber die Standard - __ne__Anwendungen __eq__?

Das Standardimplementierungsdetail von Python 3 __ne__auf C-Ebene wird verwendet, __eq__da die höhere Ebene ==( PyObject_RichCompare ) weniger effizient wäre - und daher auch behandelt werden muss NotImplemented.

Wenn __eq__es korrekt implementiert ist, ==ist auch die Negation von korrekt - und es ermöglicht uns, Implementierungsdetails auf niedriger Ebene in unserem zu vermeiden __ne__.

Mit ==ermöglicht es uns , unsere niedriges Niveau Logik in zu halten , einen Ort, und zu vermeiden Adressierung NotImplementedin __ne__.

Man könnte fälschlicherweise annehmen, dass dies ==zurückkehren könnte NotImplemented.

Es verwendet tatsächlich dieselbe Logik wie die Standardimplementierung von __eq__, die auf Identität prüft (siehe do_richcompare und unsere Beweise unten).

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

f = Foo()
f2 = Foo()

Und die Vergleiche:

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

Performance

Nehmen Sie nicht mein Wort dafür, lassen Sie uns sehen, was performanter ist:

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

Ich denke, diese Leistungszahlen sprechen für sich:

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

Dies ist sinnvoll, wenn Sie bedenken, dass low_level_pythonin Python Logik ausgeführt wird, die sonst auf C-Ebene behandelt würde.

Reaktion auf einige Kritiker

Ein anderer Antwortender schreibt:

Die Implementierung not self == otherder __ne__Methode durch Aaron Hall ist falsch, da sie niemals zurückkehren kann NotImplemented( not NotImplementedist False) und daher die __ne__Methode, die Priorität hat, niemals auf die __ne__Methode zurückgreifen kann, die keine Priorität hat.

Mit __ne__nie Rückkehr NotImplementedmacht es nicht falsch. Stattdessen behandeln wir die Priorisierung mit NotImplementedüber die Prüfung auf Gleichheit mit ==. Vorausgesetzt, es ==ist korrekt implementiert, sind wir fertig.

not self == otherwar früher die Standard-Python 3-Implementierung der __ne__Methode, aber es war ein Fehler und wurde im Januar 2015 in Python 3.4 korrigiert, wie ShadowRanger bemerkte (siehe Problem Nr. 21408).

Nun, lassen Sie uns das erklären.

Wie bereits erwähnt, behandelt Python 3 standardmäßig __ne__zuerst, ob self.__eq__(other)zurückgegeben wird NotImplemented(ein Singleton). Dies sollte überprüft isund zurückgegeben werden, wenn dies der Fall ist. Andernfalls sollte die Umkehrung zurückgegeben werden. Hier ist diese Logik, die als Klassenmixin geschrieben wurde:

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

Dies ist für die Korrektheit der Python-API auf C-Ebene erforderlich und wurde in Python 3 eingeführt

redundant. Alle relevanten __ne__Methoden wurden entfernt, einschließlich derer, die ihre eigene Prüfung implementieren, sowie solcher, die __eq__direkt oder über delegieren ==- und dies ==war die häufigste Methode.

Ist Symmetrie wichtig?

Unser hartnäckiger Kritiker liefert ein pathologisches Beispiel, um die Handhabung NotImplementedzu __ne__rechtfertigen und Symmetrie über alles zu bewerten. Lassen Sie uns das Argument mit einem klaren Beispiel stählern:

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)

Nach dieser Logik müssen wir also, um die Symmetrie aufrechtzuerhalten, die komplizierte __ne__Version unabhängig von der Python-Version schreiben .

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)

Anscheinend sollten wir uns nicht darum kümmern, dass diese Instanzen gleich und nicht gleich sind.

Ich schlage vor, dass Symmetrie weniger wichtig ist als die Annahme eines vernünftigen Codes und das Befolgen der Hinweise in der Dokumentation.

Wenn A jedoch eine vernünftige Implementierung von hätte __eq__, könnten wir hier immer noch meiner Richtung folgen und hätten immer noch Symmetrie:

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)

Fazit

Verwenden Sie ==zum Implementieren von Python 2-kompatiblem Code __ne__. Es ist mehr:

  • richtig
  • einfach
  • performant

In Python 3 nur, verwenden Sie die Low-Level - Negation auf der C - Ebene - es ist noch mehr einfache und performante (obwohl der Programmierer für die Bestimmung verantwortlich ist , dass es richtig ).

Schreiben Sie auch hier keine Low-Level-Logik in High-Level-Python.

Aaron Hall
quelle
3
Hervorragende Beispiele! Ein Teil der Überraschung ist, dass die Reihenfolge der Operanden im Gegensatz zu einigen magischen Methoden mit ihren "rechten" Reflexionen überhaupt keine Rolle spielt . So wiederholen Sie den Teil, den ich verpasst habe (und der mich viel Zeit gekostet hat): Zuerst wird die umfangreiche Vergleichsmethode der Unterklasse ausprobiert, unabhängig davon, ob der Code die Oberklasse oder die Unterklasse links vom Operator hat. Dies ist der Grund, warum Sie a1 != c2zurückgekehrt sind False- es lief nicht a1.__ne__, aber c2.__ne__das hat die Mixin- __eq__ Methode negiert . Da NotImplementedist wahr, not NotImplementedist False.
Kevin J. Chase
2
Ihre letzten Updates zeigen erfolgreich den Leistungsvorteil von not (self == other), aber niemand argumentiert, dass es nicht schnell ist (naja, schneller als jede andere Option auf Py2). Das Problem ist, dass es in einigen Fällen falsch ist; Python selbst hat dies früher getan not (self == other), sich jedoch geändert, weil es bei Vorhandensein beliebiger Unterklassen falsch war . Am schnellsten zur falschen Antwort ist immer noch falsch .
ShadowRanger
1
Das konkrete Beispiel ist wirklich unwichtig. Das Problem ist, dass in Ihrer Implementierung das Verhalten Ihrer __ne__Delegierten __eq__(bei Bedarf von beiden Seiten), aber es fällt nie auf das __ne__der anderen Seite zurück, selbst wenn beide __eq__"aufgeben". Die richtigen __ne__Delegierten an sich selbst __eq__ , aber wenn dies zurückkehrt NotImplemented, greift es zurück, um zur anderen Seite zu wechseln __ne__, anstatt die andere Seite umzukehren __eq__(da die andere Seite sich möglicherweise nicht explizit für die Delegierung __eq__entschieden hat und Sie dies nicht tun sollten diese Entscheidung dafür treffen).
ShadowRanger
1
@AaronHall: Wenn ich dies heute noch einmal überprüfe, denke ich nicht, dass Ihre Implementierung für Unterklassen normal problematisch ist (es wäre extrem kompliziert, wenn sie kaputt gehen würde, und die Unterklasse, von der angenommen wird, dass sie das übergeordnete Wissen des Elternteils besitzt, sollte dies vermeiden können ). Aber ich habe in meiner Antwort nur ein nicht verschlungenes Beispiel gegeben. Der nicht-pathologische Fall ist SQLAlchemy ORM, wo weder __eq__noch __ne__gibt entweder Trueoder False, sondern ein Proxy - Objekt (die „truthy“ passiert zu sein). Falsches Implementieren __ne__bedeutet, dass die Reihenfolge für den Vergleich von Bedeutung ist (Sie erhalten nur einen Proxy in einer Bestellung).
ShadowRanger
1
In 99% (oder vielleicht 99,999%) der Fälle ist Ihre Lösung in Ordnung und (offensichtlich) schneller. Da Sie jedoch keine Kontrolle über die Fälle haben, in denen dies nicht in Ordnung ist, müssen Sie als Bibliotheksschreiber, dessen Code von anderen verwendet werden kann (lesen Sie: alles andere als einfache einmalige Skripte und Module, die ausschließlich für den persönlichen Gebrauch bestimmt sind), dies tun Verwenden Sie die richtige Implementierung, um den allgemeinen Vertrag für die Überlastung des Bedieners einzuhalten und mit jedem anderen Code zu arbeiten, auf den Sie möglicherweise stoßen. Glücklicherweise ist auf Py3 nichts davon von Bedeutung, da Sie es __ne__ganz weglassen können . In einem Jahr wird Py2 tot sein und wir ignorieren dies. :-)
ShadowRanger
10

Nur zur Veranschaulichung __ne__: Ein kanonisch korrektes und kreuzweise tragbares Py2 / Py3-Gerät würde folgendermaßen aussehen:

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

Dies funktioniert mit allen, die __eq__Sie definieren könnten:

  • Im Gegensatz dazu not (self == other)stört es nicht in einigen nervigen / komplexen Fällen mit Vergleichen, in denen eine der beteiligten Klassen nicht impliziert, dass das Ergebnis von __ne__das gleiche ist wie das Ergebnis von noton __eq__(z. B. ORM von SQLAlchemy, wo beide __eq__und __ne__spezielle Proxy-Objekte zurückgeben). nicht Trueoder False, und der Versuch, notdas Ergebnis von zu erhalten, __eq__würde zurückkehren False, anstatt das richtige Proxy-Objekt).
  • Im Gegensatz dazu not self.__eq__(other)wird dies korrekt an die __ne__der anderen Instanz delegiert, wenn self.__eq__zurückgegeben wird NotImplemented( not self.__eq__(other)wäre besonders falsch, da NotImplementedes wahr ist, wenn __eq__also nicht gewusst wird, wie der Vergleich durchgeführt werden soll, __ne__würde dies zurückkehren False, was bedeutet, dass die beiden Objekte gleich waren , obwohl es tatsächlich das einzige war Objekt gefragt hatte keine Ahnung, was einen Ausfall von ungleich bedeuten würde)

Wenn Sie __eq__keine NotImplementedRetouren verwenden, funktioniert dies (mit bedeutungslosem Overhead). Wenn dies NotImplementedmanchmal verwendet wird, wird dies ordnungsgemäß behandelt. Und die Python-Versionsprüfung bedeutet, dass, wenn die Klasse importin Python 3 -ed __ne__ist, undefiniert bleibt, sodass Pythons native, effiziente Fallback- __ne__Implementierung (eine C-Version der oben genannten Version) die Kontrolle übernehmen kann.


Warum das nötig ist

Python-Überladungsregeln

Die Erklärung, warum Sie dies anstelle anderer Lösungen tun, ist etwas geheimnisvoll. Python hat einige allgemeine Regeln zum Überladen von Operatoren und insbesondere zu Vergleichsoperatoren:

  1. (Gilt für alle Operatoren.) LHS OP RHSVersuchen Sie es beim Ausführen LHS.__op__(RHS), und wenn dies zurückkehrt NotImplemented, versuchen Sie es RHS.__rop__(LHS). Ausnahme: Wenn RHSes sich um eine Unterklasse der LHSKlasse handelt, testen Sie RHS.__rop__(LHS) zuerst . Im Falle der Vergleichsoperatoren, __eq__und __ne__sind ihre eigenen „rop“ s (so der Prüfauftrag für __ne__ist LHS.__ne__(RHS), dann RHS.__ne__(LHS)umgekehrt , wenn RHSeine Unterklasse von ist LHS‚s - Klasse)
  2. Abgesehen von der Idee des "getauschten" Operators gibt es keine implizite Beziehung zwischen den Operatoren. Selbst zum Beispiel derselben Klasse bedeutet die LHS.__eq__(RHS)Rückgabe Truekeine LHS.__ne__(RHS)Rückgabe False(tatsächlich müssen die Operatoren nicht einmal boolesche Werte zurückgeben; ORMs wie SQLAlchemy tun dies absichtlich nicht, was eine aussagekräftigere Abfragesyntax ermöglicht). Ab Python 3 __ne__verhält sich die Standardimplementierung so, ist jedoch nicht vertraglich geregelt. Sie können __ne__auf eine Weise überschreiben , die keine strengen Gegensätze sind __eq__.

Wie dies für die Überlastung von Komparatoren gilt

Wenn Sie also einen Operator überlasten, haben Sie zwei Jobs:

  1. Wenn Sie wissen, wie Sie die Operation selbst implementieren, verwenden Sie nur Ihr eigenes Wissen über die Durchführung des Vergleichs (delegieren Sie niemals implizit oder explizit an die andere Seite der Operation; dies birgt die Gefahr von Unkorrektheit und / oder unendlicher Rekursion). je nachdem wie du es machst)
  2. Wenn Sie nicht wissen, wie Sie die Operation selbst implementieren sollen, kehren Sie immer zurück NotImplemented, damit Python an die Implementierung des anderen Operanden delegieren kann

Das Problem mit not self.__eq__(other)

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

delegiert niemals an die andere Seite (und ist falsch, wenn sie __eq__ordnungsgemäß zurückgegeben wird NotImplemented). Wenn Sie self.__eq__(other)zurückkehren NotImplemented(was "wahr" ist), kehren Sie stillschweigend zurück False, also A() != something_A_knows_nothing_aboutkehren FalseSie zurück , wenn Sie hätten prüfen sollen, ob Sie something_A_knows_nothing_aboutwissen, wie Sie mit Instanzen von vergleichen sollen A, und wenn dies nicht der Fall ist, sollten Sie zurückkehren True(da keine Seite weiß, wie im Vergleich zu den anderen gelten sie als ungleich). Wenn A.__eq__es falsch implementiert ist (Rückgabe Falsestatt NotImplementedwenn es die andere Seite nicht erkennt), dann ist dies aus Ader Sicht "korrekt" , Rückgabe True(da Aes nicht gleich ist, also nicht gleich), aber es könnte sein falsch vonsomething_A_knows_nothing_about's Perspektive, da es nie gefragt hat something_A_knows_nothing_about; A() != something_A_knows_nothing_aboutendet True, something_A_knows_nothing_about != A()könnte aber Falseoder einen anderen Rückgabewert.

Das Problem mit not self == other

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

ist subtiler. Es wird für 99% der Klassen korrekt sein, einschließlich aller Klassen, für die __ne__die logische Umkehrung von ist __eq__. Verstößt not self == otherjedoch gegen beide oben genannten Regeln, was bedeutet, dass für Klassen, bei denen dies __ne__ nicht die logische Umkehrung ist __eq__, die Ergebnisse erneut nicht symmetrisch sind, da einer der Operanden nie gefragt wird, ob er überhaupt implementiert werden kann __ne__, selbst wenn der andere Operand kann nicht. Das einfachste Beispiel ist eine verrückte Klasse, die Falsefür alle Vergleiche zurückgibt , also A() == Incomparable()und A() != Incomparable()beide zurückgeben False. Bei einer korrekten Implementierung von A.__ne__(eine, die zurückgegeben wird, NotImplementedwenn sie nicht weiß, wie der Vergleich durchgeführt werden soll) ist die Beziehung symmetrisch. A() != Incomparable()undIncomparable() != A()einigen sich auf das Ergebnis (weil im ersteren Fall A.__ne__kehrt NotImplemented, dann Incomparable.__ne__kehrt False, während in den letzteren, Incomparable.__ne__kehrt Falsedirekt). Aber wenn A.__ne__implementiert ist return not self == other, A() != Incomparable()gibt return zurück True(weil A.__eq__return nicht NotImplemented, dann Incomparable.__eq__return Falseund kehrt A.__ne__das in um True), während Incomparable() != A()return zurückkehrtFalse.

Sie können ein Beispiel dafür in Aktion sehen hier .

Offensichtlich eine Klasse, die Falsefür beide immer zurückkehrt __eq__und __ne__ein wenig seltsam ist. Aber wie bereits erwähnt, __eq__und __ne__müssen nicht einmal zurückkehren True/ False; Das SQLAlchemy ORM verfügt über Klassen mit Komparatoren, die ein spezielles Proxy-Objekt für die Abfrageerstellung zurückgeben, nicht True/ Falseüberhaupt nicht (sie sind "wahr", wenn sie in einem booleschen Kontext ausgewertet werden, aber sie sollten niemals in einem solchen Kontext ausgewertet werden).

Durch die Überlastung Fehler __ne__richtig, Sie werden brechen Klassen dieser Art, wie der Code:

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

funktioniert (vorausgesetzt, SQLAlchemy weiß, wie man überhaupt MyClassWithBadNEin eine SQL-Zeichenfolge einfügt; dies kann mit Typadaptern erfolgen, ohne MyClassWithBadNEüberhaupt zusammenarbeiten zu müssen) und übergibt das erwartete Proxy-Objekt an filter, während:

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

wird am Ende filtereine Ebene übergeben False, da self == otherein Proxy-Objekt zurückgegeben wird und not self == othernur das wahrheitsgemäße Proxy-Objekt in konvertiert wird False. Hoffentlich wird filtereine Ausnahme ausgelöst, wenn ungültige Argumente wie behandelt werden False. Während ich sicher , dass viele bin wird argumentieren , dass MyTable.fieldname sollte auf der linken Seite des Vergleichs konsequent sein, bleibt die Tatsache , dass es keinen programmatischer Grund , dies im allgemeinen Fall zu erzwingen, und eine korrekte allgemeine __ne__Wille Arbeit so oder so, während return not self == othernur Werke in einer Anordnung.

ShadowRanger
quelle
1
Die einzig richtige, vollständige und ehrliche Antwort (sorry @AaronHall). Dies sollte die akzeptierte Antwort sein.
Maggyero
4

Kurze Antwort: Ja (aber lesen Sie die Dokumentation, um es richtig zu machen)

Die Implementierung der __ne__Methode durch ShadowRanger ist die richtige (und es ist zufällig die Standardimplementierung der __ne__Methode seit Python 3.4):

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

Warum? Weil es eine wichtige mathematische Eigenschaft behält, die Symmetrie des !=Operators. Dieser Operator ist binär, daher sollte sein Ergebnis vom dynamischen Typ beider Operanden abhängen , nicht nur von einem. Dies wird über Doppelversand für Programmiersprachen implementiert, die Mehrfachversand ermöglichen (wie z. B. Julia ). In Python, das nur den Einzelversand zulässt, wird der Doppelversand für numerische Methoden und umfangreiche Vergleichsmethoden simuliert , indem der Wert NotImplementedin den Implementierungsmethoden zurückgegeben wird, die den Typ des anderen Operanden nicht unterstützen. Der Interpreter versucht dann die reflektierte Methode des anderen Operanden.

Die Implementierung not self == otherder __ne__Methode durch Aaron Hall ist falsch, da dadurch die Symmetrie des !=Operators aufgehoben wird. In der Tat kann es niemals zurückkehren NotImplemented( not NotImplementedist False) und daher kann die __ne__Methode mit höherer Priorität niemals auf die __ne__Methode mit niedrigerer Priorität zurückgreifen . not self == otherwar früher die Standard-Python 3-Implementierung der __ne__Methode, aber es war ein Fehler, der im Januar 2015 in Python 3.4 behoben wurde, wie ShadowRanger bemerkte (siehe Problem Nr. 21408 ).

Implementierung der Vergleichsoperatoren

In der Python-Sprachreferenz für Python 3 heißt es in Kapitel III Datenmodell :

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

Dies sind die sogenannten "Rich Compare" -Methoden. Die Entsprechung zwischen Operatorsymbolen und Methodennamen lautet wie folgt: x<yAnrufe x.__lt__(y), x<=yAnrufe x.__le__(y), x==yAnrufe x.__eq__(y), x!=yAnrufe x.__ne__(y), x>yAnrufe x.__gt__(y)und x>=y Anrufe x.__ge__(y).

Eine umfangreiche Vergleichsmethode kann den Singleton zurückgeben, NotImplementedwenn die Operation für ein bestimmtes Argumentpaar nicht implementiert wird.

Es gibt keine Versionen dieser Methoden mit vertauschten Argumenten (die verwendet werden sollen, wenn das linke Argument die Operation nicht unterstützt, das rechte Argument jedoch). vielmehr __lt__()und __gt__()sind die Reflexion des anderen __le__()und __ge__()sind die Reflexion des anderen und __eq__()und __ne__()sind ihre eigene Reflexion. Wenn die Operanden unterschiedlichen Typs sind und der Typ des rechten Operanden eine direkte oder indirekte Unterklasse des Typs des linken Operanden ist, hat die reflektierte Methode des rechten Operanden Priorität, andernfalls hat die Methode des linken Operanden Priorität. Virtuelle Unterklassen werden nicht berücksichtigt.

Die Übersetzung in Python-Code ergibt (mit operator_eqfor ==, operator_nefor !=, operator_ltfor <, operator_gtfor >, operator_lefor <=und operator_gefor >=):

def operator_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 operator_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 operator_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__}' and '{type(right).__name__}'")

    return result


def operator_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__}' and '{type(right).__name__}'")

    return result


def operator_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__}' and '{type(right).__name__}'")

    return result


def operator_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__}' and '{type(right).__name__}'")

    return result

Standardimplementierung der Vergleichsmethoden

Die Dokumentation fügt hinzu:

Standardmäßig __ne__()delegiert __eq__()und invertiert das Ergebnis, sofern dies nicht der Fall ist NotImplemented. Es gibt keine anderen impliziten Beziehungen zwischen den Vergleichsoperatoren, zum Beispiel (x<y or x==y)impliziert die Wahrheit von nicht x<=y.

Die Standardimplementierung der Vergleichsmethoden ( __eq__, __ne__, __lt__, __gt__, __le__und __ge__) kann so gegeben sein durch :

def __eq__(self, other):
    return 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

Dies ist also die korrekte Implementierung der __ne__Methode. Und es gibt nicht immer die Umkehrung des __eq__Verfahrens , weil , wenn die __eq__Methode zurückgibt NotImplemented, seine Umkehrung not NotImplementedist False(wie bool(NotImplemented)ist True) anstelle des gewünschten NotImplemented.

Falsche Implementierungen von __ne__

Wie Aaron Hall oben gezeigt hat, not self.__eq__(other)ist dies nicht die Standardimplementierung der __ne__Methode. Aber noch ist not self == other. Letzteres wird im Folgenden demonstriert, indem das Verhalten der Standardimplementierung mit dem Verhalten der not self == otherImplementierung in zwei Fällen verglichen wird:

  • die __eq__Methode gibt zurück NotImplemented;
  • Die __eq__Methode gibt einen anderen Wert als zurück NotImplemented.

Standardimplementierung

Mal sehen, was passiert, wenn die A.__ne__Methode die Standardimplementierung verwendet und die A.__eq__Methode zurückgibt NotImplemented:

class A:
    pass


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) == "B.__ne__"
  1. !=Anrufe A.__ne__.
  2. A.__ne__Anrufe A.__eq__.
  3. A.__eq__kehrt zurück NotImplemented.
  4. !=Anrufe B.__ne__.
  5. B.__ne__kehrt zurück "B.__ne__".

Dies zeigt, dass bei der A.__eq__Rückgabe NotImplementedder A.__ne__Methode auf die B.__ne__Methode zurückgegriffen wird.

Nun wollen wir sehen, was passiert, wenn die A.__ne__Methode die Standardimplementierung verwendet und die A.__eq__Methode einen anderen Wert zurückgibt als NotImplemented:

class A:

    def __eq__(self, other):
        return True


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. !=Anrufe A.__ne__.
  2. A.__ne__Anrufe A.__eq__.
  3. A.__eq__kehrt zurück True.
  4. !=kehrt zurück not True, das heißt False.

Dies zeigt, dass in diesem Fall die A.__ne__Methode die Umkehrung der Methode zurückgibt A.__eq__. Somit __ne__verhält sich die Methode wie in der Dokumentation angegeben.

Das Überschreiben der Standardimplementierung der A.__ne__Methode mit der oben angegebenen korrekten Implementierung führt zu denselben Ergebnissen.

not self == other Implementierung

Mal sehen, was passiert, wenn die Standardimplementierung der A.__ne__Methode mit der not self == otherImplementierung überschrieben wird und die A.__eq__Methode zurückgibt NotImplemented:

class A:

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


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is True
  1. !=Anrufe A.__ne__.
  2. A.__ne__Anrufe ==.
  3. ==Anrufe A.__eq__.
  4. A.__eq__kehrt zurück NotImplemented.
  5. ==Anrufe B.__eq__.
  6. B.__eq__kehrt zurück NotImplemented.
  7. ==kehrt zurück A() is B(), das heißt False.
  8. A.__ne__kehrt zurück not False, das heißt True.

Die Standardimplementierung der zurückgegebenen __ne__Methode "B.__ne__"nicht True.

Nun wollen wir sehen, was passiert, wenn die Standardimplementierung der A.__ne__Methode mit der not self == otherImplementierung überschrieben wird und die A.__eq__Methode einen anderen Wert zurückgibt als NotImplemented:

class A:

    def __eq__(self, other):
        return True

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


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. !=Anrufe A.__ne__.
  2. A.__ne__Anrufe ==.
  3. ==Anrufe A.__eq__.
  4. A.__eq__kehrt zurück True.
  5. A.__ne__kehrt zurück not True, das heißt False.

Die Standardimplementierung der __ne__Methode wurde auch Falsein diesem Fall zurückgegeben.

Da diese Implementierung das Verhalten der Standardimplementierung der __ne__Methode bei der __eq__Rückgabe der Methode nicht repliziert NotImplemented, ist sie falsch.

Maggyero
quelle
Zu Ihrem letzten Beispiel: "Da diese Implementierung das Verhalten der Standardimplementierung der __ne__Methode nicht repliziert , wenn die __eq__Methode NotImplemented zurückgibt, ist sie falsch." - Adefiniert bedingungslose Gleichheit. Also , A() == B(). Also A() != B() sollte es falsch sein , und das ist es auch . Die angegebenen Beispiele sind pathologisch (dh __ne__sollten keine Zeichenfolge zurückgeben und __eq__sollten nicht davon abhängen __ne__- sondern __ne__sollten davon abhängen __eq__, was die Standarderwartung in Python 3 ist). Ich bin immer noch -1 bei dieser Antwort, bis Sie meine Meinung ändern können.
Aaron Hall
@AaronHall Aus der Referenz Sprache Python : „Eine reiche Vergleichsmethode kann die Singleton zurückkehren , NotImplementedwenn er den Betrieb für ein gegebenes Paar von Argumenten nicht implementiert Vereinbarungs. FalseUnd Trueist für einen erfolgreichen Vergleich zurückgegeben. Allerdings sind diese Methoden einen Wert zurückgeben Wenn der Vergleichsoperator in einem booleschen Kontext verwendet wird (z. B. unter der Bedingung einer if-Anweisung), ruft Python bool()den Wert auf, um festzustellen, ob das Ergebnis wahr oder falsch ist. "
Maggyero
@AaronHall Ihre Implementierung von __ne__tötet eine wichtige mathematische Eigenschaft, die Symmetrie des !=Operators. Dieser Operator ist binär, daher sollte sein Ergebnis vom dynamischen Typ beider Operanden abhängen , nicht nur von einem. Dies wird in Programmiersprachen über Doppelversand für eine Sprache, die Mehrfachversand ermöglicht, korrekt implementiert . In Python, das nur den Einzelversand zulässt, wird der Doppelversand durch Rückgabe des NotImplementedWerts simuliert .
Maggyero
Das letzte Beispiel hat zwei Klassen B, dass die Renditen eine truthy Zeichenfolge auf allen Kontrollen __ne__, und Adass die Renditen Trueauf allen Kontrollen __eq__. Dies ist ein pathologischer Widerspruch. In einem solchen Widerspruch wäre es am besten, eine Ausnahme zu machen. Ohne Kenntnis von B, Aist nicht verpflichtet , zu respektieren B‚s Umsetzung der __ne__für die Zwecke der Symmetrie. An diesem Punkt im Beispiel ist es für mich irrelevant , wie AGeräte eingesetzt werden __ne__. Bitte finden Sie einen praktischen, nicht pathologischen Fall, um Ihren Standpunkt zu verdeutlichen. Ich habe meine Antwort aktualisiert, um Sie anzusprechen.
Aaron Hall
@AaronHall Ein realistischeres Beispiel finden Sie im SQLAlchemy-Beispiel von @ShadowRanger. Beachten Sie auch, dass die Tatsache, dass Ihre Implementierung __ne__in typischen Anwendungsfällen funktioniert, es nicht richtig macht. Boeing 737 MAX Flugzeuge flogen 500.000 Flüge vor den Abstürzen…
Maggyero
-1

Wenn alle __eq__, __ne__, __lt__, __ge__, __le__, und __gt__ein Sinn für die Klasse, dann implementieren gerade __cmp__statt. Ansonsten mach, was du tust, wegen dem, was Daniel DiPaolo gesagt hat (während ich es getestet habe, anstatt es nachzuschlagen;))

Karl Knechtel
quelle
12
Die __cmp__()spezielle Methode wird in Python 3.x nicht mehr unterstützt, daher sollten Sie sich an die Verwendung der umfangreichen Vergleichsoperatoren gewöhnen.
Don O'Donnell
8
Wenn Sie sich in Python 2.7 oder 3.x befinden, ist der Dekorator functools.total_ordering ebenfalls sehr praktisch.
Adam Parkin
Danke für die Warnung. In den letzten anderthalb Jahren habe ich jedoch viele Dinge in dieser Richtung erkannt. ;)
Karl Knechtel