Question Pourquoi NotImplemented est-il évalué plusieurs fois avec l'opérateur __eq__?


Ne mélangez pas les pommes et les oranges

Le problème

Je joue avec le __eq__ l'opérateur et le NotImplemented valeur.

J'essaie de comprendre ce qui se passe quand obj1.__eq__(obj2) résultats NotImplemented et obj2.__eq__(obj1) retourne aussi NotImplemented.

Selon la réponse à Pourquoi retourner NotImplemented au lieu de générer NotImplementedErroret l'article détaillé Comment remplacer les opérateurs de comparaison en Python dans le blog "LiveJournal", le runtime doit revenir au comportement intégré (basé sur l'identité de == et !=).

Échantillon de code

Mais, en essayant l'exemple ci-dessous, il semble que j'ai plusieurs appels à __eq__ pour chaque paire d'objets.

class Apple(object):
    def __init__(self, color):
        self.color = color

    def __repr__(self):
        return "<Apple color='{color}'>".format(color=self.color)

    def __eq__(self, other):
        if isinstance(other, Apple):
            print("{self} == {other} -> OK".format(self=self, other=other))
            return self.color == other.color
        print("{self} == {other} -> NotImplemented".format(self=self, other=other))
        return NotImplemented


class Orange(object):
    def __init__(self, usage):
        self.usage = usage

    def __repr__(self):
        return "<Orange usage='{usage}'>".format(usage=self.usage)

    def __eq__(self, other):
        if isinstance(other, Orange):
            print("{self} == {other}".format(self=self, other=other))
            return self.usage == other.usage
        print("{self} == {other} -> NotImplemented".format(self=self, other=other))
        return NotImplemented

>>> apple = Apple("red")
>>> orange = Orange("juice")

>>> apple == orange
<Apple color='red'> == <Orange usage='juice'> -> NotImplemented
<Orange usage='juice'> == <Apple color='red'> -> NotImplemented
<Orange usage='juice'> == <Apple color='red'> -> NotImplemented
<Apple color='red'> == <Orange usage='juice'> -> NotImplemented
False

Comportement attendu

Je m'attendais à avoir seulement:

<Apple color='red'> == <Orange usage='juice'> -> NotImplemented
<Orange usage='juice'> == <Apple color='red'> -> NotImplemented

Retourner ensuite à la comparaison d'identité id(apple) == id(orange) -> False.


11
2017-09-18 13:54


origine


Réponses:


C'est numéro 6970 dans le tracker Python; il reste non fixé dans 2.7 et Python 3.0 et 3.1.

Ceci est causé par deux places essayer à la fois la droite et la comparaison swap quand une comparaison entre deux classes personnalisées avec __eq__ les méthodes sont exécutées.

De riches comparaisons passent par le PyObject_RichCompare() fonction, qui pour des objets de types différents (indirectement) délègue à try_rich_compare(). Dans cette fonction v et w sont les objets opérande gauche et droit, et comme les deux ont un __eq__ méthode la fonction appelle à la fois v->ob_type->tp_richcompare() et w->ob_type->tp_richcompare().

Pour les classes personnalisées, le tp_richcompare() fente est défini comme le slot_tp_richcompare() fonction, et cette fonction encore exécute __eq__ pour les deux côtés, d'abord self.__eq__(self, other) puis other.__eq__(other, self).

En fin de compte, cela signifie apple.__eq__(apple, orange) et orange.__eq__(orange, apple) est appelé pour la première tentative dans try_rich_compare(), et puis l'inverse est appelé, entraînant la orange.__eq__(orange, apple) et apple.__eq__(apple, orange) appelle comme self et other sont échangés dans slot_tp_richcompare().

Notez que le problème est limité aux instances de différentes classes personnalisées où les deux classes définir un __eq__ méthode. Si l'un des côtés n'a pas une telle méthode __eq__ n'est exécuté qu'une fois:

>>> class Pear(object):
...     def __init__(self, purpose):
...         self.purpose = purpose
...     def __repr__(self):
...         return "<Pear purpose='{purpose}'>".format(purpose=self.purpose)
...    
>>> pear = Pear("cooking")
>>> apple == pear
<Apple color='red'> == <Pear purpose='cooking'> -> NotImplemented
False
>>> pear == apple
<Apple color='red'> == <Pear purpose='cooking'> -> NotImplemented
False

Si vous avez deux instances du même type et __eq__ résultats NotImplemented, vous obtenez six comparaisons même:

>>> class Kumquat(object):
...     def __init__(self, variety):
...         self.variety = variety
...     def __repr__(self):
...         return "<Kumquat variety=='{variety}'>".format(variety=self.variety)
...     def __eq__(self, other):
...         # Kumquats are a weird fruit, they don't want to be compared with anything
...         print("{self} == {other} -> NotImplemented".format(self=self, other=other))
...         return NotImplemented
...
>>> Kumquat('round') == Kumquat('oval')
<Kumquat variety=='round'> == <Kumquat variety=='oval'> -> NotImplemented
<Kumquat variety=='oval'> == <Kumquat variety=='round'> -> NotImplemented
<Kumquat variety=='round'> == <Kumquat variety=='oval'> -> NotImplemented
<Kumquat variety=='oval'> == <Kumquat variety=='round'> -> NotImplemented
<Kumquat variety=='oval'> == <Kumquat variety=='round'> -> NotImplemented
<Kumquat variety=='round'> == <Kumquat variety=='oval'> -> NotImplemented
False

La première série de deux comparaisons a été appelée à partir d'une tentative d'optimisation; lorsque deux instances ont le même type, il vous suffit d'appeler v->tp_richcompare(v, w) et les coercitions (pour les nombres) peuvent être ignorées, après tout. Cependant, lorsque cette comparaison échoue (NotImplemented est retourné), alors le chemin standard est aussi a essayé.

La façon dont les comparaisons sont effectuées dans Python 2 est devenue assez compliquée __cmp__ La méthode de comparaison à trois voies devait encore être prise en charge; dans Python 3, avec le support de __cmp__ abandonné, il était plus facile de résoudre le problème. En tant que tel, le correctif n'a jamais été renvoyé à la version 2.7.


7
2017-10-20 07:21