web-dev-qa-db-fra.com

Python, devrais-je implémenter l'opérateur __ne __ () basé sur __eq__?

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?

73
Falmarri

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 que x!=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.

47
Daniel DiPaolo

Python, devrais-je implémenter l'opérateur __ne__() basé sur __eq__?

Réponse courte: Non. Utilisez == 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 

  • implémentation de l'opérateur __ne__() en fonction de __eq__ et 
  • ne pas implémenter __ne__ dans Python 2 du tout

fournit un comportement incorrect dans la démonstration ci-dessous.

Longue réponse

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 que x!=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'est NotImplemented.

et dans la section "quoi de neuf" , nous voyons que ce comportement a changé: 

  • != renvoie maintenant l'opposé de ==, à moins que == ne renvoie NotImplemented.

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 :

L'objet 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.


Attentes

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. 

Démonstration d'un comportement inattendu lors de la définition de __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)

Comportement prévisible:

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

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

Source Python 3

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

Mais le __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:

  • simple
  • performant
  • en Python 3 uniquement, utilisez la négation de bas niveau sur le niveau C - elle est même plus simple et performante (bien que le programmeur soit responsable de la déterminer correct).

Ne not écrivez la logique de bas niveau en Python de haut niveau.

Do not write low-level logic in high level Python.

86
Aaron Hall

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.

4
ShadowRanger