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 __eq__
fonction 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
En Python 2 , n'oubliez pas de remplacer également la __ne__
fonction, comme l' indique la documentation :
Il n'y a aucune relation implicite entre les opérateurs de comparaison. La vérité de x==y
n'implique pas que ce x!=y
soit faux. Par conséquent, lors de la définition __eq__()
, il convient également de définir de __ne__()
sorte 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
En Python 3 , cela n'est plus nécessaire, comme l' indique la documentation :
Par défaut, __ne__()
délègue __eq__()
et inverse le résultat à moins qu'il ne le soit NotImplemented
. Il n'y a pas d'autres relations implicites 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:
les classes de style classique (ou à l' ancienne ), quin'héritent pas deobject
et qui sont déclarées commeclass A:
,class A():
ouclass A(B):
où seB
trouve une classe de style classique;
classes de nouveau style , qui héritent deobject
et qui sont déclarées commeclass A(object)
ouclass A(B):
oùB
est une classe de nouveau style. Python 3 n'a que des classes de nouveau style qui sont déclarées 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, tandis que pour les classes de nouveau style, elle appelle toujours la méthode de l'opérande de sous-classe, quel que soit l'ordre des opérandes .
Voici donc, si Number
c'est une classe de style classique:
n1 == n3
appels n1.__eq__
;
n3 == n1
appels n3.__eq__
;
n1 != n3
appels n1.__ne__
;
n3 != n1
les appels n3.__ne__
.
Et si Number
c'est une classe de nouveau style:
- les deux
n1 == n3
et n3 == n1
appelez n3.__eq__
;
- les deux
n1 != n3
et n3 != n1
appelez n3.__ne__
.
Pour résoudre le problème de non-commutativité des opérateurs ==
et !=
pour les classes de style classique Python 2, les méthodes __eq__
et __ne__
doivent renvoyer la NotImplemented
valeur lorsqu'un type d'opérande n'est pas pris en charge. La documentation définit la NotImplemented
valeur comme:
Les méthodes numériques et les méthodes de comparaison riches peuvent renvoyer cette valeur si elles n'implémentent pas l'opération pour les opérandes fournis. (L'interpréteur tentera alors l'opération réfléchie, ou une autre solution de rechange, selon 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 reflétée de l' autre opérande. La documentation définit les méthodes reflétées comme:
Il n'y a pas de versions à arguments échangés de ces méthodes (à utiliser lorsque l'argument gauche ne prend pas en charge l'opération mais que l'argument droit le fait); plutôt, __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 NotImplemented:
return NotImplemented
return not x
Renvoyer la NotImplemented
valeur à la place de False
est la bonne chose à faire même pour les classes de nouveau style si la commutativité des opérateurs ==
et !=
est souhaitée 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'identifiant 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 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
is
opérateur pour distinguer l'identité de l'objet de la comparaison de valeurs.