web-dev-qa-db-fra.com

Manières élégantes de supporter l'équivalence ("égalité") dans Python classes

Lors de l'écriture de classes personnalisées, il est souvent important de permettre l'équivalence à l'aide des opérateurs == et !=. Ceci est rendu possible en Python en implémentant les méthodes spéciales __eq__ et __ne__, respectivement. Le moyen le plus simple que j'ai trouvé est la méthode suivante:

class Foo:
    def __init__(self, item):
        self.item = item

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

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

Connaissez-vous des moyens plus élégants de le faire? Connaissez-vous des inconvénients particuliers à l’utilisation de la méthode ci-dessus de comparaison de __dict__s?

Note: Un peu de clarification - lorsque __eq__ et __ne__ ne sont pas définis, vous remarquerez le comportement suivant:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

C'est-à-dire que a == b est évalué à False car il exécute réellement a is b, un test d'identité (c.-à-d. "Est-ce que a est le même objet que b?" ).

Quand __eq__ et __ne__ sont définis, vous trouverez ce comportement (qui est celui que nous recherchons):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
376
gotgenes

Considérez ce problème simple:

class Number:

    def __init__(self, number):
        self.number = number


n1 = Number(1)
n2 = Number(1)

n1 == n2 # False -- oops

Ainsi, Python utilise par défaut les identificateurs d'objet pour les opérations de comparaison:

id(n1) # 140400634555856
id(n2) # 140400634555920

Remplacer la fonction __eq__ semble résoudre le problème:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return False


n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3

Dans Python 2 , n'oubliez pas de remplacer également la fonction __ne__, car documentation indique:

Il n'y a pas de relations implicites entre les opérateurs de comparaison. La vérité de x==y n'implique pas que x!=y est faux. Par conséquent, lorsque vous définissez __eq__(), vous devez également définir __ne__() afin que les opérateurs se comportent comme prévu.

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    return not self.__eq__(other)


n1 == n2 # True
n1 != n2 # False

Dans Python 3 , ceci n'est plus nécessaire, car documentation indique:

Par défaut, __ne__() délègue à __eq__() et inverse le résultat, sauf s'il s'agit de NotImplemented. Il n'y a pas d'autre relation implicite entre les opérateurs de comparaison, par exemple, la vérité de (x<y or x==y) n'implique pas x<=y.

Mais cela ne résout pas tous nos problèmes. Ajoutons une sous-classe:

class SubNumber(Number):
    pass


n3 = SubNumber(1)

n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False

Remarque: Python 2 a deux types de classes:

  • style classique (ou classes anciennes ) qui font pas hériter de object et qui sont déclarés comme class A:, class A(): ou class A(B):B est une classe de style classique;

  • new-style Les classes qui héritent de object et qui sont déclarées comme class A(object) ou class A(B):B est une classe d'un nouveau style. Python 3 n'a que des classes de style nouveau déclarées comme class A:, class A(object): ou class A(B):.

Pour les classes de style classique, une opération de comparaison appelle toujours la méthode du premier opérande, tandis que pour les classes de style nouveau, elle appelle toujours la méthode de l'opérande de sous-classe, quel que soit l'ordre des opérandes .

Donc, ici, si Number est une classe de style classique:

  • n1 == n3 appelle n1.__eq__;
  • n3 == n1 appelle n3.__eq__;
  • n1 != n3 appelle n1.__ne__;
  • n3 != n1 appelle n3.__ne__.

Et si Number est une classe de style nouveau:

  • n1 == n3 et n3 == n1 appellent n3.__eq__;
  • n1 != n3 et n3 != n1 appellent n3.__ne__.

Pour résoudre le problème de non-commutativité des opérateurs == et != pour Python 2 classes de style classique, les méthodes __eq__ et __ne__ doivent renvoyer la valeur NotImplemented lorsqu'un type d'opérande n'est pas pris en charge. La documentation définit la valeur NotImplemented comme suit:

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ète essaiera ensuite l'opération reflétée ou une autre solution de secours, en fonction de l'opérateur.) Sa valeur de vérité est vraie.

Dans ce cas, l'opérateur délègue l'opération de comparaison à la méthode réfléchie de l'opérande autre . Le documentation définit les méthodes reflétées comme:

Il n’existe pas de versions à arguments swappées de ces méthodes (à utiliser lorsque l’argument gauche ne prend pas en charge l’opération mais que l’argument droit le fait); __lt__() et __gt__() sont le reflet de l'autre, __le__() et __ge__() sont le reflet de l'autre et __eq__() et __ne__() sont leur propre reflet. .

Le résultat ressemble à ceci:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return NotImplemented

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    x = self.__eq__(other)
    if x is not NotImplemented:
        return not x
    return NotImplemented

Renvoyer la valeur NotImplemented au lieu de False est la bonne chose à faire même pour les classes de style nouveau si la commutativité de == et != opérateurs sont souhaités lorsque les opérandes sont de types non liés (pas d'héritage).

Sommes-nous déjà là? Pas assez. Combien de numéros uniques avons-nous?

len(set([n1, n2, n3])) # 3 -- oops

Les ensembles utilisent les hachages des objets et par défaut, Python renvoie le hachage de l'identificateur de l'objet. Essayons de le remplacer:

def __hash__(self):
    """Overrides the default implementation"""
    return hash(Tuple(sorted(self.__dict__.items())))

len(set([n1, n2, n3])) # 1

Le résultat final ressemble à ceci (j'ai ajouté quelques assertions à la fin pour validation):

class Number:

    def __init__(self, number):
        self.number = number

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Number):
            return self.number == other.number
        return NotImplemented

    def __ne__(self, other):
        """Overrides the default implementation (unnecessary in Python 3)"""
        x = self.__eq__(other)
        if x is not NotImplemented:
            return not x
        return NotImplemented

    def __hash__(self):
        """Overrides the default implementation"""
        return hash(Tuple(sorted(self.__dict__.items())))


class SubNumber(Number):
    pass


n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)

assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1

assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1

assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1

assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2
280
Tal Weiss

Vous devez faire attention à l'héritage:

>>> class Foo:
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

>>> class Bar(Foo):pass

>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False

Vérifiez les types plus strictement, comme ceci:

def __eq__(self, other):
    if type(other) is type(self):
        return self.__dict__ == other.__dict__
    return False

En plus de cela, votre approche fonctionnera bien, c'est pourquoi les méthodes spéciales sont là.

191
Algorias

Vous décrivez comme je l’ai toujours fait. Comme il est totalement générique, vous pouvez toujours diviser cette fonctionnalité en une classe mixin et l'hériter dans des classes où vous le souhaitez.

class CommonEqualityMixin(object):

    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)

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

class Foo(CommonEqualityMixin):

    def __init__(self, item):
        self.item = item
157
cdleary

Pas une réponse directe, mais semblait assez pertinente pour être rajoutée car cela évite parfois un ennui verbeux. Couper directement de la docs ...


functools.total_ordering (cls)

Étant donné qu'une classe définissant une ou plusieurs méthodes de classement par comparaison enrichies, ce décorateur de classe fournit le reste. opérations:

La classe doit définir l'un des éléments __lt__(), __le__(), __gt__() ou __ge__(). De plus, la classe devrait fournir une méthode __eq__().

Nouveau dans la version 2.7

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))
13
John Mee

Vous n'êtes pas obligé de remplacer __eq__ et __ne__ vous ne pouvez remplacer que __cmp__, mais cela aura une incidence sur le résultat de ==,! ==, <,>, etc. sur.

is teste l'identité de l'objet. Cela signifie que is b sera True dans le cas où a et b contiennent tous deux la référence au même objet. Dans python, vous tenez toujours une référence à un objet dans une variable et non à l'objet réel. Par conséquent, pour que a soit b, les objets doivent être situés dans le même emplacement mémoire. Comment et surtout, pourquoi voudriez-vous renverser ce comportement?

Edit: Je ne savais pas que __cmp__ avait été supprimé de python 3, donc évitez-le.

8
Vasil

À partir de cette réponse: https://stackoverflow.com/a/30676267/541136 J'ai démontré que, bien qu'il soit correct de définir __ne__ en termes __eq__ - au lieu de

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

tu devrais utiliser:

def __ne__(self, other):
    return not self == other
5
Aaron Hall

Je pense que les deux termes que vous recherchez sont égalité (==) et identité (est). Par exemple:

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True       <-- a and b have values which are equal
>>> a is b
False      <-- a and b are not the same list object
2
too much php

Le test 'is' testera l'identité à l'aide de la fonction intégrée 'id ()' qui renvoie essentiellement l'adresse de la mémoire de l'objet et ne peut donc pas être surchargée.

Cependant, dans le cas du test d'égalité d'une classe, vous voudrez probablement être un peu plus strict à propos de vos tests et comparer uniquement les attributs de données de votre classe:

import types

class ComparesNicely(object):

    def __eq__(self, other):
        for key, value in self.__dict__.iteritems():
            if (isinstance(value, types.FunctionType) or 
                    key.startswith("__")):
                continue

            if key not in other.__dict__:
                return False

            if other.__dict__[key] != value:
                return False

         return True

Ce code ne comparera que les membres de votre classe qui ne sont pas des données de fonction, ainsi que tout ce qui est privé, ce qui est généralement ce que vous voulez. Dans le cas de Plain Old Python Objects, j'ai une classe de base qui implémente __init__, __str__, __repr__ et __eq__ afin que mes objets POPO ne supportent pas le fardeau de cette logique supplémentaire (et dans la plupart des cas identique) .

1
mcrute

Au lieu d'utiliser des sous-classes/mixins, j'aime utiliser un décorateur de classe générique

def comparable(cls):
    """ Class decorator providing generic comparison functionality """

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__

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

    cls.__eq__ = __eq__
    cls.__ne__ = __ne__
    return cls

Usage:

@comparable
class Number(object):
    def __init__(self, x):
        self.x = x

a = Number(1)
b = Number(1)
assert a == b
0
bluenote10