façons élégantes pour soutenir l'équivalence ( « égalité ») dans les classes Python

StackOverflow https://stackoverflow.com/questions/390250

  •  23-08-2019
  •  | 
  •  

Question

Lors de l'écriture des classes personnalisées, il est souvent important de permettre l'équivalence au moyen des opérateurs de == et !=. En Python, ceci est rendu possible par la mise en œuvre des __eq__ et __ne__ méthodes spéciales, respectivement. La meilleure façon que je l'ai trouvé pour ce faire 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 la comparaison __dict__s?

Remarque : Un peu de précisions - quand __eq__ et __ne__ ne sont pas définies, vous trouverez ce comportement:

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

C'est, a == b évalue à False parce qu'il fonctionne vraiment a is b, un test d'identité (à savoir, "Est-ce a le même objet que b?").

Lorsque __eq__ et __ne__ sont définis, vous trouverez ce comportement (qui est celui que nous sommes après):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True
Était-ce utile?

La solution

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

Outrepasser 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

Python 2 , souvenez-vous toujours de passer outre la fonction __ne__ ainsi, comme documentation états:

  

Il n'y a pas de relations implicites entre les opérateurs de comparaison. le   vérité de x==y ne signifie pas que x!=y est faux. En conséquence, lorsque   définissant __eq__(), il faut également définir __ne__() de sorte que le   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

Python 3 , ce n'est plus nécessaire, comme documentation indique:

  

Par défaut, les délégués __ne__() à __eq__() et inversera le résultat   à moins qu'il soit NotImplemented. Il n'y a pas d'autres sous-entendus   les relations entre les opérateurs de comparaison, par exemple, la vérité   de (x<y or x==y) ne signifie 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:

  • Classic- le style (ou style ancien ) classes, qui ne pas héritant 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- le style classes, qui ne héritent de object et qui sont déclarés comme class A(object) ou class A(B):B est une nouvelle classe de style. Python 3 ne dispose que de nouvelles classes de style qui sont déclarés 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, alors que pour les classes de type nouveau, il appelle toujours la méthode de l'opérande de sous-classe, quel que soit l'ordre des opérandes .

Voici donc, 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 nouvelle classe de style:

  • les deux n1 == n3 et n3 == n1 appel n3.__eq__;
  • les deux n1 != n3 et n3 != n1 n3.__ne__ d'appel.

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

  

Méthodes numériques et les méthodes de comparaison riches peuvent retourner cette valeur si   ils ne mettent pas en œuvre l'opération pour les opérandes fournis. (Le   interprète essayera alors l'opération réfléchie, ou un autre   fallback, selon l'opérateur). Sa valeur de vérité est vraie.

Dans ce cas, les délégués de l'opérateur de l'opération de comparaison à la méthode réfléchie de autre opérande. Le définit les méthodes reflètent que:

  

Il n'y a pas des versions de ces arguments permutés méthodes (à utiliser   lorsque l'argument gauche ne supporte pas l'opération, mais le droit   l'argument ne); plutôt, __lt__() et __gt__() sont uns des autres   réflexion, __le__() et __ge__() sont la réflexion de l'autre, et   __eq__() et __ne__() sont leur propre réflexion.

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

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

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

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

Ensembles utilisent les hachages d'objets, et par défaut Python renvoie la valeur de hachage de l'identificateur de l'objet. Essayons de la 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 affirmations à la fin de la 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

Autres conseils

Vous devez être prudent avec 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 outre, votre approche fonctionne très bien, c'est ce que les méthodes spéciales sont là pour.

La façon dont vous décrivez est la façon dont je l'ai toujours fait. Comme il est tout à fait générique, vous pouvez toujours briser cette fonctionnalité dans une classe à mixin et hériter dans les classes où vous voulez que la fonctionnalité.

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

Pas une réponse directe, mais semblait suffisamment pertinente pour être clouée sur car il permet d'économiser un peu d'ennui bavard à l'occasion. Couper directement à partir de la documentation ...


functools.total_ordering (cls)

Étant donné une classe définissant une ou plusieurs riches méthodes de commande de comparaison, ce décorateur de classe fournit le reste Cela simplifie l'effort nécessaire pour spécifier toutes les opérations de comparaison riches possibles.

La classe doit définir l'un des __lt__(), __le__(), __gt__() ou __ge__(). En outre, la classe doit 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()))

Vous ne devez pas passer outre à la fois __eq__ et __ne__ vous pouvez remplacer seulement __cmp__ mais cela fera une incidence sur le résultat de ==,! ==, <,> et ainsi de suite.

Tests de is pour l'identité de l'objet. Cela signifie un is b sera True dans le cas où a et b deux tiennent la référence au même objet. En python vous maintenez toujours une référence à un objet dans une variable non l'objet réel, donc essentiellement a est b pour être vrai les objets en eux devraient être situés dans le même emplacement mémoire. Comment et surtout pourquoi iriez-vous sur le remplacement de ce comportement?

Modifier. Je ne savais pas __cmp__ a été retiré de Python 3 afin éviter

De cette réponse: https://stackoverflow.com/a/30676267/541136 J'ai démontré que, tout en il est correct de définir __ne__ en termes __eq__ - au lieu de

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

vous devez utiliser:

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

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

« est » test pour tester l'identité en utilisant la commande interne 'id () fonction qui retourne essentiellement l'adresse de mémoire de l'objet et est donc pas surchargeable.

Toutefois, dans le cas de tester l'égalité d'une classe que vous voulez sans doute être un peu plus strict sur vos tests et seulement comparer les attributs de données dans 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 membres de données non fonction de votre classe, ainsi que sauter quoi que ce soit privé qui est généralement ce que vous voulez. Dans le cas d'objets Plain Old Python J'ai une classe de base qui implémente __init__, __str__, __repr__ et __eq__ donc mes objets Popo ne portent pas la charge de tout ce supplémentaire logique (et dans la plupart des cas identiques).

Au lieu d'utiliser / mixins sous-classement, je préfère 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

Utilisation:

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

a = Number(1)
b = Number(1)
assert a == b
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top