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

98

J'ai une classe dans laquelle je veux remplacer l' __eq__()opérateur. Il semble logique que je devrais également remplacer l' __ne__()opérateur, mais est-il judicieux d'implémenter __ne__en __eq__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 dans la façon dont Python utilise ces opérateurs qui en fait une mauvaise idée?

Falmarri
la source

Réponses:

57

Oui, c'est parfaitement bien. En fait, la documentation vous invite à définir __ne__quand vous définissez __eq__:

Il n'y a pas de relations implicites entre les opérateurs de comparaison. La vérité de x==yn'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.

Dans de nombreux cas (comme celui-ci), ce sera aussi simple que de nier le résultat __eq__, mais pas toujours.

Daniel DiPaolo
la source
12
c'est la bonne réponse (en bas ici, par @ aaron-hall). La documentation que vous citez ne pas vous encourager à mettre en œuvre à l' __ne__aide __eq__, seulement que vous le mettre en œuvre.
guyarad
2
@guyarad: En fait, la réponse d'Aaron est encore un peu fausse grâce à une mauvaise délégation; au lieu de traiter un NotImplementedretour d'un côté comme un signal à déléguer de __ne__l'autre côté, not self == otherc'est (en supposant que l'opérande __eq__ne sait pas comment comparer l'autre opérande) déléguer implicitement __eq__de l'autre côté, puis l'inverser. Pour les types étranges, par exemple les champs de SQLAlchemy ORM, cela pose des problèmes .
ShadowRanger
1
La critique de ShadowRanger ne s'appliquerait qu'aux cas très pathologiques (IMHO) et est entièrement traitée dans ma réponse ci-dessous.
Aaron Hall
1
Les documentations les plus récentes (pour 3.7 au moins, pourraient être encore plus anciennes) __ne__sont automatiquement déléguées à __eq__et la citation de cette réponse n'existe plus dans les documents. En bout de ligne, il est parfaitement pythonique de ne mettre en œuvre __eq__et de laisser __ne__déléguer.
bluesummers
132

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==yn'implique pas que ce x!=ysoit 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 NotImplementedpour le type vérifié, Python vérifiera de manière appropriée other.__eq__(self) Dans la documentation :

L' NotImplementedobjet

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 otherest un sous - type, et si elle a défini cet opérateur, il utilise la otherméthode de la première (inverse pour <, <=, >=et >). Si NotImplementedest 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.cdansobject_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 NotImplementeddans __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_pythonc'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 == otherde la __ne__méthode par Aaron Hall est incorrecte car elle ne peut jamais retourner NotImplemented( not NotImplementedest 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 NotImplementedne le rend pas incorrect. Au lieu de cela, nous gérons la priorisation avec NotImplementedvia 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 iset 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 NotImplementeden __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.

Salle Aaron
la source
3
Excellents exemples! Une partie de la surprise est que l'ordre des opérandes n'a pas du tout d'importance , contrairement à certaines méthodes magiques avec leurs réflexions «du côté droit». Pour réitérer la partie que j'ai manquée (et qui m'a coûté beaucoup de temps): La méthode de comparaison riche de la sous - classe est essayée en premier, que le code ait la superclasse ou la sous-classe à gauche de l'opérateur. C'est pourquoi votre a1 != c2retour False--- il n'a pas fonctionné a1.__ne__, mais c2.__ne__, ce qui a annulé la méthode du mixin __eq__ . Depuis NotImplementedest la vérité, not NotImplementedest False.
Kevin J.Chase
2
Vos mises à jour récentes démontrent avec succès les avantages de performance de not (self == other), mais personne ne prétend que ce n'est pas rapide (enfin, plus rapide que toute autre option sur Py2 de toute façon). Le problème est que c'est faux dans certains cas; Python lui-même avait l'habitude de faire not (self == other), mais a changé parce qu'il était incorrect en présence de sous-classes arbitraires . Le plus rapide à la mauvaise réponse est toujours faux .
ShadowRanger
1
L'exemple spécifique est vraiment sans importance. Le problème est que, dans votre implémentation, le comportement de vos __ne__délégués à __eq__(des deux côtés si nécessaire), mais il ne retombe jamais__ne__ de l'autre côté même lorsque les deux __eq__"abandonnent". Le bon __ne__délègue à son propre __eq__ , mais si cela revient NotImplemented, il retombe pour aller de l'autre côté __ne__, plutôt que d'inverser l'autre côté __eq__(puisque l'autre côté n'a peut-être pas explicitement choisi de déléguer à __eq__, et vous ne devriez pas prendre cette décision pour lui).
ShadowRanger
1
@AaronHall: En réexaminant cela aujourd'hui, je ne pense pas que votre implémentation soit normalement problématique pour les sous-classes (il serait extrêmement compliqué de la faire casser, et la sous-classe, supposée avoir une connaissance complète du parent, devrait être en mesure de l'éviter ). Mais je viens de donner un exemple non alambiqué dans ma réponse. Le cas non pathologique est l'ORM de SQLAlchemy, où ni __eq__ni ne __ne__retourne ni Trueni False, mais plutôt un objet proxy (qui se trouve être "véridique"). Une mise en œuvre incorrecte __ne__signifie que l'ordre est important pour la comparaison (vous n'obtenez un proxy que dans un ordre).
ShadowRanger
1
Pour être clair, dans 99% (ou peut-être 99,999%) des cas, votre solution est bonne et (évidemment) plus rapide. Mais comme vous n'avez pas de contrôle sur les cas où cela ne va pas , en tant qu'écrivain de bibliothèque dont le code peut être utilisé par d'autres (lire: tout sauf de simples scripts et modules uniques uniquement pour un usage personnel), vous devez utilisez l'implémentation correcte pour adhérer au contrat général de surcharge des opérateurs et travaillez avec tout autre code que vous pourriez rencontrer. Heureusement, sur Py3, rien de tout cela n'a d'importance, car vous pouvez omettre __ne__complètement. Dans un an, Py2 sera mort et nous l'ignorons. :-)
ShadowRanger
10

Pour mémoire, un portable Py2 / Py3 canoniquement correct et croisé __ne__ressemblerait à:

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__ce que vous pourriez définir:

  • Contrairement à not (self == other), n'interfère pas avec certains cas ennuyeux / complexes impliquant des comparaisons où l'une des classes impliquées n'implique pas que le résultat de __ne__est le même que celui de noton __eq__(par exemple l'ORM de SQLAlchemy, où les deux __eq__et __ne__retournent des objets proxy spéciaux, pas Trueou False, et en essayant notle résultat de __eq__retournerait False, plutôt que le bon objet proxy).
  • Contrairement à not self.__eq__(other)cela, cela délègue correctement à l' __ne__autre instance lorsque self.__eq__retourne NotImplemented( not self.__eq__(other)serait très faux, car NotImplementedc'est la vérité, donc quand __eq__je ne savais pas comment effectuer la comparaison, __ne__reviendrait False, ce qui implique que les deux objets étaient égaux alors qu'en fait le seul l'objet demandé n'avait aucune idée, ce qui impliquerait un défaut de différent)

Si vous __eq__n'utilisez pas de NotImplementedretours, cela fonctionne (avec une surcharge insignifiante), s'il l'utilise NotImplementedparfois, cela le gère correctement. Et la vérification de la version de Python signifie que si la classe est import-ed dans Python 3, elle __ne__n'est pas définie, ce qui permet à l' __ne__implémentation de secours native et efficace de Python (une version C de ce qui précède) de prendre le relais.


Pourquoi cela est nécessaire

Règles de surcharge Python

L'explication de la raison pour laquelle vous faites cela au lieu d'autres solutions est quelque peu obscure. Python a quelques règles générales sur la surcharge des opérateurs, et des opérateurs de comparaison en particulier:

  1. (S'applique à tous les opérateurs) Lors de l'exécution LHS OP RHS, essayez LHS.__op__(RHS), et si cela revient NotImplemented, essayez RHS.__rop__(LHS). Exception: si RHSest une sous-classe de LHSla classe de, alors testez d' RHS.__rop__(LHS) abord . Dans le cas des opérateurs de comparaison, __eq__et __ne__sont leurs propres "cordes" (donc l'ordre de test pour __ne__est LHS.__ne__(RHS), alors RHS.__ne__(LHS), inversé si RHSest une sous-classe de LHSla classe de)
  2. Hormis l'idée de l'opérateur "swapped", il n'y a pas de relation implicite entre les opérateurs. Même par exemple de la même classe, le LHS.__eq__(RHS)retour Truen'implique pas de LHS.__ne__(RHS)retours False(en fait, les opérateurs ne sont même pas obligés de retourner des valeurs booléennes; les ORM comme SQLAlchemy ne le font pas intentionnellement, ce qui permet une syntaxe de requête plus expressive). Depuis Python 3, l' __ne__implémentation par défaut se comporte de cette façon, mais ce n'est pas contractuel; vous pouvez remplacer __ne__par des moyens qui ne sont pas strictement opposés à __eq__.

Comment cela s'applique à la surcharge des comparateurs

Ainsi, lorsque vous surchargez un opérateur, vous avez deux tâches:

  1. Si vous savez comment implémenter l'opération vous-même, faites-le, en utilisant uniquement vos propres connaissances sur la façon de faire la comparaison (ne jamais déléguer, implicitement ou explicitement, à l'autre côté de l'opération; cela risque de provoquer des erreurs et / ou une récursivité infinie, selon comment tu le fais)
  2. Si vous ne savez pas comment implémenter l'opération vous-même, retournez toujoursNotImplemented , afin que Python puisse déléguer à l'implémentation de l'autre opérande

Le problème avec not self.__eq__(other)

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

ne délègue jamais à l'autre côté (et est incorrect s'il __eq__retourne correctement NotImplemented). Quand self.__eq__(other)revient NotImplemented(ce qui est "véridique"), vous retournez silencieusement False, donc A() != something_A_knows_nothing_aboutretourne False, quand il aurait dû vérifier s'il something_A_knows_nothing_aboutsavait comment comparer aux instances de A, et si ce n'est pas le cas, il aurait dû revenir True(car si aucune des deux parties ne sait comment comparer les uns aux autres, ils sont considérés comme différents les uns des autres). Si A.__eq__est incorrectement implémenté (retourne Falseau lieu de NotImplementedquand il ne reconnaît pas l'autre côté), alors c'est "correct" du Apoint de vue de, retournant True(puisque Ane pense pas que c'est égal, donc ce n'est pas égal), mais cela pourrait être faux desomething_A_knows_nothing_aboutle point de vue de, puisqu'il n'a même jamais demandé something_A_knows_nothing_about; A() != something_A_knows_nothing_aboutse termine True, mais something_A_knows_nothing_about != A()pourrait False, ou toute autre valeur de retour.

Le problème avec not self == other

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

est plus subtile. Cela va être correct pour 99% des classes, y compris toutes les classes pour lesquelles __ne__est l'inverse logique de __eq__. Mais not self == otherenfreint les deux règles mentionnées ci-dessus, ce qui signifie que pour les classes où __ne__ n'est pas l'inverse logique de __eq__, les résultats sont à nouveau non symétriques, car on ne demande jamais à l'un des opérandes s'il peut implémenter __ne__du tout, même si l'autre l'opérande ne peut pas. L'exemple le plus simple est une classe bizarre qui retourne Falsepour toutes les comparaisons, donc A() == Incomparable()et les A() != Incomparable()deux retournent False. Avec une implémentation correcte de A.__ne__(une qui retourne NotImplementedquand elle ne sait pas comment faire la comparaison), la relation est symétrique; A() != Incomparable()etIncomparable() != A()d'accord sur le résultat (car dans le premier cas, A.__ne__retourne NotImplemented, puis Incomparable.__ne__retourne False, tandis que dans le second, Incomparable.__ne__retourne Falsedirectement). Mais quand A.__ne__est implémenté en tant que return not self == other, A() != Incomparable()renvoie True(car A.__eq__renvoie, non NotImplemented, puis Incomparable.__eq__retourne Falseet A.__ne__inverse cela à True), tandis que Incomparable() != A()renvoieFalse.

Vous pouvez voir un exemple de cela en action ici .

Évidemment, une classe qui revient toujours Falsepour les deux __eq__et qui __ne__est un peu étrange. Mais comme mentionné précédemment, __eq__et vous __ne__n'avez même pas besoin de retourner True/ False; l'ORM SQLAlchemy a des classes avec des comparateurs qui retournent un objet proxy spécial pour la construction de requêtes, pas True/ pas Falsedu tout (ils sont "véridiques" s'ils sont évalués dans un contexte booléen, mais ils ne sont jamais censés être évalués dans un tel contexte).

En ne surcharge pas __ne__correctement, vous allez briser les classes de ce genre, comme le code:

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

fonctionnera (en supposant que SQLAlchemy sache comment insérer MyClassWithBadNEdans une chaîne SQL du tout; cela peut être fait avec des adaptateurs de type sans MyClassWithBadNEavoir à coopérer du tout), en passant l'objet proxy attendu à filter, tandis que:

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

finira par passer filterun simple False, car self == otherretourne un objet proxy, et not self == otherconvertit simplement l'objet proxy de vérité en False. Heureusement, filterlève une exception sur le traitement d'arguments invalides comme False. Bien que je sois sûr que beaucoup diront que cela MyTable.fieldname devrait être systématiquement sur le côté gauche de la comparaison, le fait demeure qu'il n'y a aucune raison programmatique pour appliquer cela dans le cas général, et un générique correct __ne__fonctionnera dans les deux cas, alors qu'il return not self == otherne fonctionne que dans un seul arrangement.

ShadowRanger
la source
1
La seule réponse correcte, complète et honnête (désolé @AaronHall). Cela devrait être la réponse acceptée.
Maggyero le
4

Réponse courte: oui (mais lisez la documentation pour bien faire les choses)

L'implémentation de la __ne__méthode par ShadowRanger est la bonne (et il se trouve que c'est l'implémentation par défaut de la __ne__méthode depuis Python 3.4):

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

    if result is not NotImplemented:
        return not result

    return NotImplemented

Pourquoi? Parce qu'il garde une propriété mathématique importante, la symétrie de l' !=opérateur. Cet opérateur est binaire, donc son résultat doit dépendre du type dynamique des deux opérandes, pas seulement d'un. Ceci est implémenté via une double répartition pour les langages de programmation permettant une répartition multiple (comme Julia ). En Python qui n'autorise qu'une seule distribution, la double distribution est simulée pour les méthodes numériques et les méthodes de comparaison riches en renvoyant la valeur NotImplementeddans les méthodes d'implémentation qui ne prennent pas en charge le type de l'autre opérande; l'interpréteur essaiera alors la méthode reflétée de l'autre opérande.

L'implémentation not self == otherde la __ne__méthode par Aaron Hall est incorrecte car elle supprime la symétrie de l' !=opérateur. En effet, il ne peut jamais retourner NotImplemented( not NotImplementedest False) et donc la __ne__méthode avec une priorité plus élevée ne peut jamais se rabattre sur la __ne__méthode avec une priorité inférieure. Auparavant, il s'agissait not self == otherde l'implémentation Python 3 par défaut de la __ne__méthode, mais c'était un bogue qui a été corrigé dans Python 3.4 en janvier 2015, comme ShadowRanger l'a remarqué (voir le numéro 21408 ).

Implémentation des opérateurs de comparaison

La référence du langage Python pour Python 3 indique dans son chapitre III Modèle de données :

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

Ce sont les méthodes dites de «comparaison riche». La correspondance entre les symboles d'opérateur et les noms de méthode est la suivante: x<yappels x.__lt__(y), x<=yappels x.__le__(y), x==yappels x.__eq__(y), x!=yappels x.__ne__(y), x>yappels x.__gt__(y)et x>=y appels x.__ge__(y).

Une méthode de comparaison riche peut renvoyer le singleton NotImplementedsi elle n'implémente pas l'opération pour une paire d'arguments donnée.

Il n'y a pas de versions à argument permuté de ces méthodes (à utiliser lorsque l'argument de gauche ne prend pas en charge l'opération mais que l'argument de droite le fait); plutôt, __lt__()et __gt__()sont le reflet de chacun, __le__()et __ge__()sont le reflet de l'autre, __eq__()et __ne__()sont leur propre reflet. Si les opérandes sont de types différents et que le type de l'opérande droit est une sous-classe directe ou indirecte du type de l'opérande gauche, la méthode reflétée de l'opérande droit a la priorité, sinon la méthode de l'opérande gauche a la priorité. Le sous-classement virtuel n'est pas pris en compte.

Traduire cela en code Python donne (en utilisant operator_eqfor ==, operator_nefor !=, operator_ltfor <, operator_gtfor >, operator_lefor <=et 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

Implémentation par défaut des méthodes de comparaison

La documentation ajoute:

Par défaut, __ne__()délègue __eq__()et inverse le résultat, sauf si c'est le cas NotImplemented. Il n'y a pas d'autres relations implicites entre les opérateurs de comparaison, par exemple, la vérité de (x<y or x==y)n'implique pas x<=y.

La mise en œuvre par défaut des méthodes de comparaison ( __eq__, __ne__, __lt__, __gt__, __le__et __ge__) peut donc être donnée par:

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

C'est donc la bonne implémentation de la __ne__méthode. Et il ne revient pas toujours l'inverse de la __eq__méthode , car lorsque la __eq__méthode retourne NotImplemented, son inverse not NotImplementedest False(comme bool(NotImplemented)est True) au lieu du produit souhaité NotImplemented.

Implémentations incorrectes de __ne__

Comme Aaron Hall l'a démontré ci-dessus, ce not self.__eq__(other)n'est pas l'implémentation par défaut de la __ne__méthode. Mais non plus not self == other. Ce dernier est démontré ci-dessous en comparant le comportement de l'implémentation par défaut avec le comportement de l' not self == otherimplémentation dans deux cas:

  • la __eq__méthode retourne NotImplemented;
  • la __eq__méthode renvoie une valeur différente de NotImplemented.

Implémentation par défaut

Voyons ce qui se passe lorsque la A.__ne__méthode utilise l'implémentation par défaut et que la A.__eq__méthode retourne NotImplemented:

class A:
    pass


class B:

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


assert (A() != B()) == "B.__ne__"
  1. !=appels A.__ne__.
  2. A.__ne__appels A.__eq__.
  3. A.__eq__revient NotImplemented.
  4. !=appels B.__ne__.
  5. B.__ne__revient "B.__ne__".

Cela montre que lorsque la A.__eq__méthode retourne NotImplemented, la A.__ne__méthode retombe sur la B.__ne__méthode.

Voyons maintenant ce qui se passe lorsque la A.__ne__méthode utilise l'implémentation par défaut et que la A.__eq__méthode renvoie une valeur différente de NotImplemented:

class A:

    def __eq__(self, other):
        return True


class B:

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


assert (A() != B()) is False
  1. !=appels A.__ne__.
  2. A.__ne__appels A.__eq__.
  3. A.__eq__revient True.
  4. !=revient not True, c'est-à-dire False.

Cela montre que dans ce cas, la A.__ne__méthode renvoie l'inverse de la A.__eq__méthode. Ainsi, la __ne__méthode se comporte comme annoncé dans la documentation.

Le remplacement de l'implémentation par défaut de la A.__ne__méthode par l'implémentation correcte donnée ci-dessus donne les mêmes résultats.

not self == other la mise en oeuvre

Voyons ce qui se passe lors du remplacement de l'implémentation par défaut de la A.__ne__méthode avec l' not self == otherimplémentation et la A.__eq__méthode retourne 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. !=appels A.__ne__.
  2. A.__ne__appels ==.
  3. ==appels A.__eq__.
  4. A.__eq__revient NotImplemented.
  5. ==appels B.__eq__.
  6. B.__eq__revient NotImplemented.
  7. ==revient A() is B(), c'est-à-dire False.
  8. A.__ne__revient not False, c'est-à-dire True.

L'implémentation par défaut de la __ne__méthode renvoyée "B.__ne__", non True.

Voyons maintenant ce qui se passe lors du remplacement de l'implémentation par défaut de la A.__ne__méthode par l' not self == otherimplémentation et la A.__eq__méthode renvoie une valeur différente de 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. !=appels A.__ne__.
  2. A.__ne__appels ==.
  3. ==appels A.__eq__.
  4. A.__eq__revient True.
  5. A.__ne__revient not True, c'est-à-dire False.

L'implémentation par défaut de la __ne__méthode est également renvoyée Falsedans ce cas.

Étant donné que cette implémentation ne parvient pas à répliquer le comportement de l'implémentation par défaut de la __ne__méthode lorsque la __eq__méthode retourne NotImplemented, elle est incorrecte.

Maggyero
la source
Pour votre dernier exemple: «Étant donné que cette implémentation ne parvient pas à répliquer le comportement de l'implémentation par défaut de la __ne__méthode lorsque la __eq__méthode renvoie NotImplemented, il est incorrect.» - Adéfinit l'égalité inconditionnelle. Ainsi, A() == B(). Ainsi A() != B() devrait être faux , et il est . Les exemples donnés sont pathologiques (c'est-à-dire __ne__qu'ils ne doivent pas renvoyer de chaîne, et __eq__ne doivent pas dépendre de __ne__- ils __ne__doivent plutôt dépendre de __eq__, qui est l'attente par défaut dans Python 3). Je suis toujours -1 sur cette réponse jusqu'à ce que vous puissiez changer d'avis.
Aaron Hall
@AaronHall De la référence du langage Python : "Une méthode de comparaison riche peut renvoyer le singleton NotImplementedsi elle n'implémente pas l'opération pour une paire d'arguments donnée. Par convention, Falseet Truesont renvoyées pour une comparaison réussie. Cependant, ces méthodes peuvent renvoyer n'importe quelle valeur , donc si l'opérateur de comparaison est utilisé dans un contexte booléen (par exemple, dans la condition d'une instruction if), Python appellera bool()la valeur pour déterminer si le résultat est vrai ou faux. "
Maggyero
@AaronHall Votre implémentation de __ne__tue une propriété mathématique importante, la symétrie de l' !=opérateur. Cet opérateur est binaire, donc son résultat doit dépendre du type dynamique des deux opérandes, pas d'un seul. Ceci est correctement implémenté dans les langages de programmation via le double envoi pour le langage permettant l' envoi multiple . En Python qui n'autorise qu'une seule distribution, la double distribution est simulée en renvoyant la NotImplementedvaleur.
Maggyero le
Le dernier exemple a deux classes B,, qui renvoie une chaîne de vérité sur toutes les vérifications de __ne__, et Aqui retourne Truesur toutes les vérifications de __eq__. C'est une contradiction pathologique. Dans une telle contradiction, il serait préférable de soulever une exception. A l'insu de B, An'est pas tenu de respecter Bla mise en œuvre de __ne__à des fins de symétrie. À ce stade de l'exemple, la façon dont les Aoutils __ne__ne m'intéresse pas. Veuillez trouver un cas pratique et non pathologique pour faire valoir votre point de vue. J'ai mis à jour ma réponse pour m'adresser à vous.
Aaron Hall
@AaronHall Pour un exemple plus réaliste, consultez l'exemple SQLAlchemy donné par @ShadowRanger. Notez également que le fait que votre implémentation __ne__fonctionne dans des cas d'utilisation typiques ne le rend pas juste. Les Boeing 737 MAX ont effectué 500 000 vols avant les accidents…
Maggyero
-1

Si tous __eq__, __ne__, __lt__, __ge__, __le__et du __gt__sens pour la classe, puis juste mettre en œuvre à la __cmp__place. Sinon, faites ce que vous faites, à cause du commentaire de Daniel DiPaolo (pendant que je le testais au lieu de le rechercher;))

Karl Knechtel
la source
12
La __cmp__()méthode spéciale n'est plus prise en charge dans Python 3.x, vous devez donc vous habituer à utiliser les opérateurs de comparaison enrichis.
Don O'Donnell
8
Ou bien si vous êtes en Python 2.7 ou 3.x, le décorateur functools.total_ordering est également très pratique.
Adam Parkin du
Merci pour l'information. Cependant, j'ai réalisé beaucoup de choses dans ce sens depuis un an et demi. ;)
Karl Knechtel