Considera questo semplice problema:
class Number:
def __init__(self, number):
self.number = number
n1 = Number(1)
n2 = Number(1)
n1 == n2 # False -- oops
Quindi, Python per impostazione predefinita utilizza gli identificatori di oggetti per le operazioni di confronto:
id(n1) # 140400634555856
id(n2) # 140400634555920
Sostituire la __eq__
funzione sembra risolvere il problema:
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
In Python 2 , ricorda sempre di sovrascrivere anche la __ne__
funzione, come il documentazione afferma:
Non ci sono relazioni implicite tra gli operatori di confronto. La verità di x==y
non implica che x!=y
sia falso. Di conseguenza, quando si definisce __eq__()
, si dovrebbe anche definire in __ne__()
modo che gli operatori si comporteranno come previsto.
def __ne__(self, other):
"""Overrides the default implementation (unnecessary in Python 3)"""
return not self.__eq__(other)
n1 == n2 # True
n1 != n2 # False
In Python 3 , questo non è più necessario, in quanto la documentazione afferma:
Per impostazione predefinita, __ne__()
delega __eq__()
e inverte il risultato a meno che non lo sia NotImplemented
. Non vi sono altre relazioni implicite tra gli operatori di confronto, ad esempio la verità di (x<y or x==y)
non implica x<=y
.
Ma ciò non risolve tutti i nostri problemi. Aggiungiamo una sottoclasse:
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
Nota: Python 2 ha due tipi di classi:
classi di stile classico (o vecchio stile ), che non ereditano daobject
e che sono dichiarate comeclass A:
,class A():
oclass A(B):
dove siB
trova una classe di stile classico;
classi di nuovo stile , che ereditano daobject
e che sono dichiarate comeclass A(object)
oclass A(B):
dove siB
trova una classe di nuovo stile. Python 3 ha solo classi di nuovo stile dichiarate comeclass A:
,class A(object):
oclass A(B):
.
Per le classi di stile classico, un'operazione di confronto chiama sempre il metodo del primo operando, mentre per le classi di nuovo stile, chiama sempre il metodo dell'operando di sottoclasse, indipendentemente dall'ordine degli operandi .
Quindi qui, se Number
è una classe in stile classico:
n1 == n3
chiamate n1.__eq__
;
n3 == n1
chiamate n3.__eq__
;
n1 != n3
chiamate n1.__ne__
;
n3 != n1
chiamate n3.__ne__
.
E se Number
è una classe di nuovo stile:
- entrambi
n1 == n3
e n3 == n1
chiama n3.__eq__
;
- entrambi
n1 != n3
e n3 != n1
chiama n3.__ne__
.
Per risolvere il problema di non commutatività degli operatori ==
e !=
per le classi di stile classico di Python 2, i metodi __eq__
e __ne__
dovrebbero restituire il NotImplemented
valore quando un tipo di operando non è supportato. La documentazione definisce il NotImplemented
valore come:
I metodi numerici e i metodi di confronto avanzato possono restituire questo valore se non implementano l'operazione per gli operandi forniti. (L'interprete tenterà quindi l'operazione riflessa, o qualche altro fallback, a seconda dell'operatore.) Il suo valore di verità è vero.
In questo caso i delegati operatore l'operazione di confronto al metodo riflesse del altro operando. La documentazione definisce i metodi riflessi come:
Non ci sono versioni di argomenti scambiate di questi metodi (da usare quando l'argomento left non supporta l'operazione ma l'argomento right lo fa); piuttosto, __lt__()
e __gt__()
sono il riflesso dell'altro, __le__()
e __ge__()
sono il riflesso dell'altro, e
__eq__()
e__ne__()
sono il loro riflesso.
Il risultato è simile al seguente:
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
Restituire il NotImplemented
valore invece di False
è la cosa giusta da fare anche per le classi di nuovo stile se la commutatività di ==
e!=
si desidera la operatori quando gli operandi sono di tipo non correlato (nessuna eredità).
Siamo arrivati? Non proprio. Quanti numeri univoci abbiamo?
len(set([n1, n2, n3])) # 3 -- oops
I set utilizzano gli hash degli oggetti e, per impostazione predefinita, Python restituisce l'hash dell'identificatore dell'oggetto. Proviamo a sovrascriverlo:
def __hash__(self):
"""Overrides the default implementation"""
return hash(tuple(sorted(self.__dict__.items())))
len(set([n1, n2, n3])) # 1
Il risultato finale è simile al seguente (ho aggiunto alcune asserzioni alla fine per la convalida):
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
operatore per distinguere l'identità dell'oggetto dal confronto di valori.