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
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 quex!=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 deNotImplemented
. 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 pasx<=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):
où 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):
où 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
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à.
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
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()))
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.
À 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
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
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) .
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