J'ai une classe où je veux remplacer l'opérateur __eq__()
. Il semble logique de devoir remplacer l'opérateur __ne__()
également, mais est-il judicieux de mettre en œuvre __ne__
en fonction de __eq__
en tant que tel?
class A:
def __eq__(self, other):
return self.value == other.value
def __ne__(self, other):
return not self.__eq__(other)
Ou y a-t-il quelque chose qui me manque avec la façon dont Python utilise ces opérateurs qui fait que ce n'est pas une bonne idée?
Oui, c'est parfaitement bien. En fait, la documentation vous invite à définir __ne__
lorsque vous définissez __eq__
:
Il n'y a pas de relations implicites parmi les opérateurs de comparaison. Le la vérité de
x==y
n'implique pas quex!=y
c'est faux. En conséquence, lors de la définition de__eq__()
, il convient également de définir__ne__()
pour que les opérateurs se comportent comme prévu.
Dans beaucoup de cas (comme celui-ci), ce sera aussi simple que d’annuler le résultat de __eq__
, mais pas toujours.
Python, devrais-je implémenter l'opérateur
__ne__()
basé sur__eq__
?
==
au lieu du __eq__
En Python 3, !=
est la négation de ==
par défaut. Vous n'êtes donc même pas obligé d'écrire un __ne__
et la documentation n'est plus utilisée pour en écrire un.
En règle générale, pour le code Python 3 uniquement, n’en écrivez pas, sauf si vous devez éclipser l’implémentation parent, par exemple. pour un objet intégré.
C'est-à-dire, gardez à l'esprit commentaire de Raymond Hettinger :
La méthode
__ne__
découle automatiquement de__eq__
uniquement si__ne__
n'est pas déjà défini dans une superclasse. Donc, si vous êtes héritant d'un mode intégré, il est préférable de remplacer les deux.
Si vous avez besoin que votre code fonctionne en Python 2, suivez la recommandation pour Python 2 et tout se passera bien en 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 ==
au lieu du __eq__
. E.G.
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__()
en fonction de __eq__
et __ne__
dans Python 2 du toutfournit un comportement incorrect dans la démonstration ci-dessous.
La documentation pour Python 2 dit:
Il n'y a pas de relations implicites entre les opérateurs de comparaison. Le la vérité de
x==y
n'implique pas quex!=y
est faux. En conséquence, quand Si vous définissez__eq__()
, vous devez également définir__ne__()
pour que le les opérateurs se comporteront comme prévu.
Cela signifie donc que si nous définissons __ne__
en fonction 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'estNotImplemented
.
et dans la section "quoi de neuf" , nous voyons que ce comportement a changé:
!=
renvoie maintenant l'opposé de==
, à moins que==
ne renvoieNotImplemented
.
Pour implémenter __ne__
, nous préférons utiliser l'opérateur ==
au lieu d'utiliser directement la méthode __eq__
. Ainsi, si self.__eq__(other)
d'une sous-classe renvoie NotImplemented
pour le type vérifié, Python vérifiera correctement other.__eq__(self)
d'après la documentation :
NotImplemented
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 à condition de. (L’interprète essaiera alors l’opération reflétée ou Une autre solution de repli, en fonction de l’opérateur.) Sa valeur de vérité est vrai.
Lorsqu'un opérateur de comparaison enrichi est attribué, s'ils ne sont pas du même type, Python vérifie si la other
est un sous-type et, s'il a défini cet opérateur, il utilise d'abord la méthode other
(inverse pour <
, <=
, >=
et >
). . Si NotImplemented
est renvoyé, then, il utilise la méthode opposée. (Not ne vérifie pas la même méthode deux fois.) L'utilisation de l'opérateur ==
permet à cette logique de se dérouler.
Sémantiquement, vous devez implémenter __ne__
en termes de vérification de l'égalité, car les utilisateurs de votre classe s'attendent à 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
C'est-à-dire que les deux fonctions ci-dessus devraient toujours renvoyer le même résultat. Mais cela dépend du programmeur.
__ne__
sur la base de __eq__
: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."""
Instancier 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)
(Remarque: Bien que chaque assertion sur deux de chacun des éléments ci-dessous soit équivalente et donc logiquement redondante par rapport à celle qui la précède, je les inclut pour démontrer que order n'a pas d'importance si l'une est une sous-classe de l'autre.
__ne__
est implémenté avec ==
dans ces instances:
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 __ne__
est implémenté 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!
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__
dans Python 2Pour vous assurer que vous ne devez pas ignorer l'implémentation de __ne__
dans Python 2, consultez les objets équivalents suivants:
>>> 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
!
L'implémentation CPython par défaut pour __ne__
est dans typeobject.c
DANS 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);
}
}
Ici on voit
__ne__
par défaut utilise __eq__
?Les détails d'implémentation __ne__
par défaut de Python 3 au niveau C utilisent __eq__
, car le niveau supérieur ==
( PyObject_RichCompare ) serait moins efficace - et par conséquent, il doit également gérer NotImplemented
.Si __eq__
est correctement implémenté, la négation de ==
est également correcte - et nous permet d’éviter les détails d’implémentation de bas niveau dans notre __ne__
.
Utiliser ==
nous permet de conserver notre logique de bas niveau à la place _/one et de eviter l'adressage NotImplemented
dans __ne__
.
On pourrait supposer à tort que ==
peut renvoyer 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()
>>> f == f True >>> f != f False >>> f2 == f False >>> f2 != f True
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
>>> 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
Conclusion.
Pour le code compatible Python 2, utilisez
==
pour implémenter__ne__
. C'est plus:
Ne not écrivez la logique de bas niveau en Python de haut niveau.
Do not write low-level logic in high level Python.
Pour mémoire, un __ne__
portable, canoniquement correct et croisé, ressemblerait à ceci:
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
Cela fonctionne avec tout __eq__
que vous pourriez définir et contrairement à not (self == other)
, n'interfère pas dans certains cas agaçants/complexes impliquant des comparaisons entre des instances où une instance appartient à une sous-classe de l'autre. Si votre __eq__
n'utilise pas les retours NotImplemented
, cela fonctionne (avec une surcharge inutile), s'il utilise parfois NotImplemented
, cela le gère correctement. Et la vérification de la version Python signifie que si la classe est import
- éd en Python 3, __ne__
n'est pas défini, permettant à l'implémentation native et efficace de repli __ne__
de Python (une version C de ce qui précède) de prendre en charge.