it-swarm.com.de

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

Ich habe eine Klasse, in der ich den Operator __eq__() überschreiben möchte. Es scheint sinnvoll zu sein, dass ich auch den Operator __ne__() überschreiben sollte, aber ist es sinnvoll, __ne__ basierend auf __eq__ als solchen zu implementieren?

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

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

Oder fehlt mir etwas bei der Art und Weise, wie Python diese Operatoren verwendet, was dazu führt, dass dies keine gute Idee ist?

73
Falmarri

Ja, das ist vollkommen in Ordnung. die Dokumentation fordert Sie auf, __ne__ zu definieren, wenn Sie __eq__ definieren:

Es gibt keine implizierten Beziehungen unter den Vergleichsoperatoren. Das Die Wahrheit von x==y bedeutet nicht, dass x!=y ist falsch. Dementsprechend bei der Definition von __eq__(), man sollte auch __ne__() definieren, damit sich die Operatoren wie erwartet verhalten.

In vielen Fällen (wie in diesem Fall) wird es so einfach sein, das Ergebnis von __eq__ zu negieren, jedoch nicht immer.

47
Daniel DiPaolo

Python, sollte ich den __ne__() -Operator basierend auf __eq__ Implementieren?

Kurze Antwort: Nein. Verwenden Sie == Anstelle von __eq__.

In Python 3 ist != Standardmäßig die Negation von ==, Sodass Sie nicht einmal __ne__ Schreiben müssen mehr meinte beim Schreiben eines.

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

Das heißt, bedenken Sie Raymond Hettingers Kommentar :

Die Methode __ne__ Folgt nur dann automatisch aus __eq__, Wenn __ne__ Noch nicht in einer Oberklasse definiert ist. Wenn Sie also von einem eingebauten System erben, ist es am besten, beide zu überschreiben.

Wenn Sie Ihren Code für Python 2 benötigen, 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 den __ne__ In Bezug auf == Definieren. anstelle von __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

  • implementieren des __ne__() -Operators basierend auf __eq__ und
  • implementierung von __ne__ in Python 2 überhaupt nicht

bietet falsches Verhalten in der unten stehenden Demonstration.

Lange Antwort

Die Dokumentation für Python 2 sagt:

Es gibt keine impliziten Beziehungen zwischen den Vergleichsoperatoren. Die Wahrheit von x==y Impliziert nicht, dass x!=y Falsch ist. Dementsprechend sollte man beim Definieren von __eq__() auch __ne__() definieren, damit sich die Operatoren wie erwartet verhalten.

Das heißt, wenn wir __ne__ Als Inverse von __eq__ Definieren, können wir ein konsistentes Verhalten erzielen.

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

Standardmäßig delegiert __ne__() an __eq__() und invertiert das Ergebnis, sofern es nicht NotImplemented ist.

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, == Gibt NotImplemented zurück.

Für die Implementierung von __ne__ Bevorzugen wir die Verwendung des Operators == anstelle der direkten Verwendung der Methode __eq__, Sodass if self.__eq__(other) einer Unterklasse gibt NotImplemented für den geprüften Typ zurück, Python prüft other.__eq__(self)Aus der Dokumentation :

Das Objekt NotImplemented

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

Wenn ein Rich-Vergleichsoperator angegeben wird, prüft Python, ob der other ein Subtyp ist, und wenn dieser Operator definiert ist, wird der other 's Methode zuerst (invers für <, <=, >= und >). Wenn NotImplemented zurückgegeben wird, then wird die gegenteilige Methode verwendet. (Es wird nicht zweimal nach der gleichen Methode gesucht.) Die Verwendung des Operators == Ermöglicht, dass diese Logik ausgeführt wird.


Erwartungen

Semantisch gesehen sollten Sie __ne__ In Bezug auf die Prüfung auf Gleichheit implementieren, da Benutzer Ihrer Klasse erwarten, dass die folgenden Funktionen für alle Instanzen von A gleich 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 dasselbe Ergebnis zurückgeben. Dies ist jedoch vom Programmierer abhängig.

Demonstration eines unerwarteten Verhaltens beim Definieren von __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."""

Instanziiere nicht äquivalente Instanzen:

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: Obwohl jede zweite Behauptung der folgenden Aussagen äquivalent und daher logisch redundant zu der vorherigen ist, schließe ich sie ein, um zu demonstrieren, dass Reihenfolge keine Rolle spielt, wenn eine Unterklasse der anderen ist.)

Diese Instanzen haben __ne__ Mit == Implementiert:

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__ Mit __eq__ Implementiert haben - obwohl 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 darauf, dass Sie die Implementierung von __ne__ In Python 2 nicht überspringen sollten, finden Sie in den folgenden 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 False sein!

Python 3-Quelle

Die Standard-CPython-Implementierung für __ne__ Ist in typeobject.c In object_richcompare :

    case Py_NE:
        /* By default, __ne__() delegates to __eq__() and inverts the result,
           unless the latter returns NotImplemented. */
        if (self->ob_type->tp_richcompare == NULL) {
            res = Py_NotImplemented;
            Py_INCREF(res);
            break;
        }
        res = (*self->ob_type->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);
            }
        }

Hier sehen wir

Aber die Standardeinstellung __ne__ Verwendet __eq__?

Das Standard-Implementierungsdetail von Python 3 __ne__ Auf C-Ebene verwendet __eq__, Da die höhere Ebene == ( PyObject_RichCompare ) weniger effizient wäre - und daher auch muss auch mit NotImplemented umgehen.

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

Die Verwendung von == Ermöglicht es uns, unsere Logik auf niedriger Ebene an one Stelle zu belassen und avoidNotImplemented in __ne__.

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

Tatsächlich verwendet es dieselbe Logik wie die Standardimplementierung von __eq__, Die nach Identität sucht (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

Nimm mein Wort nicht dafür, lass 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_python In Python eine Logik ausführt, die sonst auf C-Ebene behandelt würde.

Reaktion auf einige Kritiker

Ein anderer Antwortender schreibt:

Aaron Halls Implementierung not self == other Der __ne__ - Methode ist falsch, da sie niemals NotImplemented zurückgeben kann (not NotImplemented Ist False) und daher die __ne__ - Methode, die Priorität hat, kann niemals auf die __ne__ - Methode zurückgreifen, die keine Priorität hat.

Wenn __ne__ Nie NotImplemented zurückgibt, ist dies nicht falsch. Stattdessen behandeln wir die Priorisierung mit NotImplemented über die Prüfung auf Gleichheit mit ==. Vorausgesetzt, == Ist korrekt implementiert, sind wir fertig.

not self == other War die standardmäßige 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 Ausgabe Nr. 21408).

Nun, lassen Sie uns das erklären.

Wie bereits erwähnt, behandelt Python 3 standardmäßig __ne__, Indem zuerst geprüft wird, ob self.__eq__(other)NotImplemented (ein Singleton) zurückgibt - was mit überprüft werden sollte is und zurückgegeben, wenn dies der Fall ist, sollte das Gegenteil zurückgegeben werden. Hier ist diese Logik als Klassenmix geschrieben:

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 Richtigkeit der C-Level-API Python erforderlich und wurde in Python 3 eingeführt

redundant. Alle relevanten __ne__ - Methoden wurden entfernt, einschließlich der Methoden, die ihre eigene Prüfung implementieren, sowie der Methoden, die direkt oder über __eq__ An == Delegieren - und == Lauteten die gebräuchlichste Art, dies zu tun.

Fazit

Verwenden Sie für Python 2-kompatiblen Code ==, Um __ne__ Zu implementieren. Es ist mehr:

  • richtig
  • einfach
  • performant

Verwenden Sie nur in Python 3 die Negation auf niedriger Ebene auf der C-Ebene - sie ist sogar more einfach und performant (obwohl der Programmierer dafür verantwortlich ist, zu bestimmen, dass es = ist richtig).

Schreiben Sie wieder nicht Low-Level-Logik in High-Level-Python.

101
Aaron Hall

Nur für den Rekord würde ein kanonisch korrekter und tragbarer __ne__ von Py2/Py3 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 jedem __eq__, den Sie möglicherweise definieren, und stört im Gegensatz zu not (self == other) nicht in einigen ärgerlichen/komplexen Fällen, in denen Vergleiche zwischen Fällen gemacht werden, in denen eine Instanz eine Unterklasse der anderen ist. Wenn Ihr __eq__ keine NotImplemented Returns verwendet, funktioniert dies (mit sinnlosem Aufwand), wenn NotImplemented manchmal verwendet wird, wird dies ordnungsgemäß behandelt. Und die Überprüfung der Python-Version bedeutet, dass, wenn die Klasse in Python 3 imported ist, __ne__ undefiniert bleibt, wodurch die native, effiziente Fallback-Implementierung __ne__ von Python (eine C-Version des obigen) übernommen werden kann.

4
ShadowRanger