Python, dois-je implémenter un __ne__()
opérateur basé sur __eq__
?
Réponse courte: Ne pas mettre en œuvre, mais si vous devez, utiliser ==
, non__eq__
Dans Python 3, !=
est la négation de ==
par défaut, donc vous n'êtes même pas obligé d'écrire un __ne__
, et la documentation n'est plus d'avis sur l'écriture d'un.
De manière générale, pour le code Python 3 uniquement, n'en écrivez pas à moins que vous n'ayez besoin d'éclipser l'implémentation parent, par exemple pour un objet intégré.
Autrement dit, gardez à l'esprit le commentaire de Raymond Hettinger :
La __ne__
méthode découle automatiquement de __eq__
seulement si elle
__ne__
n'est pas déjà définie dans une superclasse. Donc, si vous héritez d'une fonction prédéfinie, il est préférable de remplacer les deux.
Si vous avez besoin que votre code fonctionne dans Python 2, suivez la recommandation pour Python 2 et cela fonctionnera très bien dans Python 3.
Dans Python 2, Python lui-même n'implémente automatiquement aucune opération en termes d'une autre - par conséquent, vous devez définir le __ne__
en termes de ==
plutôt que de __eq__
. PAR EXEMPLE
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)`
Voir la preuve que
__ne__()
opérateur de mise en œuvre basé sur __eq__
et
- pas du tout implémenté
__ne__
dans Python 2
fournit un comportement incorrect dans la démonstration ci-dessous.
Longue réponse
La documentation de Python 2 dit:
Il n'y a pas de relations implicites entre les opérateurs de comparaison. La vérité de x==y
n'implique pas que ce x!=y
soit faux. En conséquence, lors de la définition __eq__()
, il convient également de définir de __ne__()
sorte que les opérateurs se comportent comme prévu.
Cela signifie donc que si nous définissons __ne__
en termes de l'inverse de __eq__
, nous pouvons obtenir un comportement cohérent.
Cette section de la documentation a été mise à jour pour Python 3:
Par défaut, __ne__()
délègue __eq__()
et inverse le résultat, sauf si c'est le cas NotImplemented
.
et dans la section "Quoi de neuf" , nous voyons que ce comportement a changé:
!=
renvoie maintenant l'opposé de ==
, sauf si ==
retourne NotImplemented
.
Pour l'implémentation __ne__
, nous préférons utiliser l' ==
opérateur au lieu d'utiliser la __eq__
méthode directement afin que si self.__eq__(other)
une sous-classe retourne NotImplemented
pour le type vérifié, Python vérifiera de manière appropriée other.__eq__(self)
Dans la documentation :
L' NotImplemented
objet
Ce type a une valeur unique. Il y a un seul objet avec cette valeur. Cet objet est accessible via le nom intégré
NotImplemented
. Les méthodes numériques et les méthodes de comparaison enrichies peuvent renvoyer cette valeur si elles n'implémentent pas l'opération pour les opérandes fournis. (L'interpréteur essaiera alors l'opération reflétée, ou une autre solution de secours, selon l'opérateur.) Sa valeur de vérité est true.
Administré un opérateur de comparaison riche, si elles ne sont pas du même type, Python vérifie si le other
est un sous - type, et si elle a défini cet opérateur, il utilise la other
méthode de la première (inverse pour <
, <=
, >=
et >
). Si NotImplemented
est renvoyé, alors il utilise la méthode opposée. (Il ne vérifie pas deux fois la même méthode.) L'utilisation de l' ==
opérateur permet à cette logique de se produire.
Attentes
Sémantiquement, vous devez implémenter __ne__
en termes de vérification d'égalité car les utilisateurs de votre classe s'attendront à ce que les fonctions suivantes soient équivalentes pour toutes les instances de 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
Autrement dit, les deux fonctions ci-dessus doivent toujours renvoyer le même résultat. Mais cela dépend du programmeur.
Démonstration d'un comportement inattendu lors de la définition __ne__
basée sur __eq__
:
Tout d'abord la configuration:
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."""
Instanciez des instances non équivalentes:
right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)
Comportement prévisible:
(Remarque: bien que chaque seconde assertion de chacun des éléments ci-dessous soit équivalente et donc logiquement redondante à celle qui la précède, je les inclue pour démontrer que l' ordre n'a pas d'importance lorsque l'un est une sous-classe de l'autre. )
Ces instances ont été __ne__
implémentées avec ==
:
assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1
Ces instances, testées sous Python 3, fonctionnent également correctement:
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
Et rappelez-vous que ceux-ci ont été __ne__
implémentés avec __eq__
- bien que ce soit le comportement attendu, l'implémentation est incorrecte:
assert not wrong1 == wrong2 # These are contradicted by the
assert not wrong2 == wrong1 # below unexpected behavior!
Comportement inattendu:
Notez que cette comparaison contredit les comparaisons ci-dessus ( not wrong1 == wrong2
).
>>> assert wrong1 != wrong2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
et,
>>> assert wrong2 != wrong1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
Ne sautez pas __ne__
dans Python 2
Pour obtenir des preuves que vous ne devriez pas ignorer l'implémentation __ne__
dans Python 2, consultez ces objets équivalents:
>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True
Le résultat ci-dessus devrait être False
!
Source Python 3
L'implémentation CPython par défaut pour __ne__
est typeobject.c
dansobject_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;
Mais les __ne__
utilisations par défaut __eq__
?
Les __ne__
détails d'implémentation par défaut de Python 3 au niveau C sont utilisés __eq__
car le niveau supérieur ==
( PyObject_RichCompare ) serait moins efficace - et doit donc également gérer NotImplemented
.
Si __eq__
est correctement implémenté, alors la négation de ==
est également correcte - et cela nous permet d'éviter les détails d'implémentation de bas niveau dans notre __ne__
.
L' utilisation ==
nous permet de garder notre logique de bas niveau dans un endroit, et éviter d' adressage NotImplemented
dans __ne__
.
On pourrait supposer à tort que cela ==
peut revenir NotImplemented
.
Il utilise en fait la même logique que l'implémentation par défaut de __eq__
, qui vérifie l'identité (voir do_richcompare et nos preuves ci-dessous)
class Foo:
def __ne__(self, other):
return NotImplemented
__eq__ = __ne__
f = Foo()
f2 = Foo()
Et les comparaisons:
>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True
Performance
Ne me croyez pas sur parole, voyons ce qui est le plus performant:
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
Je pense que ces chiffres de performance parlent d'eux-mêmes:
>>> 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
Cela a du sens quand on considère que low_level_python
c'est faire de la logique en Python qui serait autrement gérée au niveau C.
Réponse à certains critiques
Un autre répondeur écrit:
L'implémentation not self == other
de la __ne__
méthode par Aaron Hall est incorrecte car elle ne peut jamais retourner NotImplemented
( not NotImplemented
est False
) et donc la __ne__
méthode qui a la priorité ne peut jamais se rabattre sur la __ne__
méthode qui n'a pas de priorité.
Ne __ne__
jamais revenir NotImplemented
ne le rend pas incorrect. Au lieu de cela, nous gérons la priorisation avec NotImplemented
via la vérification de l'égalité avec ==
. En supposant qu'il ==
soit correctement implémenté, nous avons terminé.
not self == other
__ne__
Auparavant, c'était l' implémentation Python 3 par défaut de la méthode, mais c'était un bogue et il a été corrigé dans Python 3.4 en janvier 2015, comme ShadowRanger l'a remarqué (voir le problème # 21408).
Eh bien, expliquons cela.
Comme indiqué précédemment, Python 3 gère par défaut __ne__
en vérifiant d'abord si self.__eq__(other)
retourne NotImplemented
(un singleton) - qui devrait être vérifié avec is
et retourné si c'est le cas, sinon il devrait renvoyer l'inverse. Voici cette logique écrite comme un mixin de classe:
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
Ceci est nécessaire pour l'exactitude de l'API Python de niveau C, et il a été introduit dans Python 3, faisant
redondant. Toutes les __ne__
méthodes pertinentes ont été supprimées, y compris celles qui implémentent leur propre contrôle ainsi que celles qui délèguent __eq__
directement ou via ==
- et ==
c'était la manière la plus courante de le faire.
La symétrie est-elle importante?
Notre critique persistante fournit un exemple pathologique pour justifier la manipulation NotImplemented
en __ne__
, valorisant la symétrie avant tout. Traitons l'argument avec un exemple clair:
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)
Donc, par cette logique, afin de maintenir la symétrie, nous devons écrire le compliqué __ne__
, quelle que soit la version de Python.
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)
Apparemment, nous ne devons pas penser que ces instances sont à la fois égales et non égales.
Je propose que la symétrie soit moins importante que la présomption de code sensible et suivant les conseils de la documentation.
Cependant, si A avait une implémentation sensée de __eq__
, alors nous pourrions toujours suivre ma direction ici et nous aurions toujours une symétrie:
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)
Conclusion
Pour le code compatible Python 2, utilisez ==
pour implémenter __ne__
. C'est plus:
- correct
- Facile
- performant
En Python 3 uniquement, utilisez la négation de bas niveau au niveau C - c'est encore plus simple et performant (bien que le programmeur soit responsable de déterminer si elle est correcte ).
Encore une fois, n'écrivez pas de logique de bas niveau en Python de haut niveau.
__ne__
aide__eq__
, seulement que vous le mettre en œuvre.