façons élégantes pour soutenir l'équivalence ( « égalité ») dans les classes Python
-
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
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 quex!=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 soitNotImplemented
. 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 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:
-
Classic- le style (ou style ancien ) classes, qui ne pas héritant de
object
et qui sont déclarés commeclass A:
,class A():
ouclass A(B):
oùB
est une classe de style classique; -
New- le style classes, qui ne héritent de
object
et qui sont déclarés commeclass A(object)
ouclass A(B):
oùB
est une nouvelle classe de style. Python 3 ne dispose que de nouvelles classes de style qui sont déclarés commeclass A:
,class A(object):
ouclass 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
appellen1.__eq__
; -
n3 == n1
appellen3.__eq__
; -
n1 != n3
appellen1.__ne__
; -
n3 != n1
appellen3.__ne__
.
Et si Number
est une nouvelle classe de style:
- les deux
n1 == n3
etn3 == n1
appeln3.__eq__
; - les deux
n1 != n3
etn3 != 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