Modi eleganti per supportare l'equivalenza ("uguaglianza") nelle classi Python


421

Quando si scrivono classi personalizzate è spesso importante consentire l'equivalenza mediante gli operatori ==e !=. In Python, ciò è reso possibile mediante l'attuazione delle __eq__e __ne__particolari metodi, rispettivamente. Il modo più semplice che ho trovato per fare questo è il seguente metodo:

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)

Conosci mezzi più eleganti per farlo? Conoscete qualche particolare svantaggio nell'uso del metodo sopra riportato per confrontare __dict__s?

Nota : un po 'di chiarimento: quando __eq__e __ne__non sono definiti, troverai questo comportamento:

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

Cioè, a == bvaluta Falseperché è veramente in esecuzione a is b, un test di identità (cioè "È alo stesso oggetto di b?").

Quando __eq__e __ne__sono definiti, troverai questo comportamento (che è quello che stiamo cercando):

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

6
+1, poiché non sapevo che dict usasse l'uguaglianza dei membri per ==, avevo ipotizzato che li contasse solo uguali per gli stessi oggetti oggetto. Immagino che questo sia ovvio poiché Python ha l' isoperatore per distinguere l'identità dell'oggetto dal confronto di valori.
SingleNegationElimination

5
Penso che la risposta accettata sia corretta o riassegnata alla risposta di Algorias, in modo che venga implementato il rigoroso controllo del tipo.
massimo

1
Assicurarsi inoltre hash è sottoposto a override stackoverflow.com/questions/1608842/...
Alex Punnen

Risposte:


328

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==ynon implica che x!=ysia 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 daobjecte che sono dichiarate comeclass A:,class A():oclass A(B):dove siBtrova una classe di stile classico;

  • classi di nuovo stile , che ereditano daobjecte che sono dichiarate comeclass A(object)oclass A(B):dove siBtrova 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 == n3chiamate n1.__eq__;
  • n3 == n1chiamate n3.__eq__;
  • n1 != n3chiamate n1.__ne__;
  • n3 != n1chiamate n3.__ne__.

E se Numberè una classe di nuovo stile:

  • entrambi n1 == n3e n3 == n1chiama n3.__eq__;
  • entrambi n1 != n3e n3 != n1chiama 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 NotImplementedvalore quando un tipo di operando non è supportato. La documentazione definisce il NotImplementedvalore 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 NotImplementedvalore 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

3
hash(tuple(sorted(self.__dict__.items())))non funzionerà se ci sono oggetti non cancellabili tra i valori di self.__dict__(ovvero, se uno degli attributi dell'oggetto è impostato, per esempio, a list).
massimo

3
È vero, ma se hai tali oggetti mutabili nei tuoi var () i due oggetti non sono davvero uguali ...
Tal Weiss,


1
Tre osservazioni: 1. In Python 3, non è più necessario implementare __ne__: "Per impostazione predefinita, __ne__()delega __eq__()e inverte il risultato a meno che non sia NotImplemented". 2. Se si vuole ancora da implementare __ne__, un'implementazione più generico (quello usato da Python 3 credo) è: x = self.__eq__(other); if x is NotImplemented: return x; else: return not x. 3. Il dato __eq__e le __ne__implementazioni non sono ottimali: if isinstance(other, type(self)):dà 22 __eq__e 10 __ne__chiamate, mentre if isinstance(self, type(other)):darebbe 16 __eq__e 6 __ne__chiamate.
Maggyero,

4
Ha chiesto eleganza, ma è diventato robusto.
GregNash,

201

Devi stare attento con l'eredità:

>>> 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

Controlla i tipi più rigorosamente, in questo modo:

def __eq__(self, other):
    if type(other) is type(self):
        return self.__dict__ == other.__dict__
    return False

Oltre a ciò, il tuo approccio funzionerà bene, ecco a cosa servono metodi speciali.


Questo è un buon punto. Suppongo che valga la pena notare che la suddivisione in classi dei tipi predefiniti consente comunque l'uguaglianza in entrambe le direzioni, e quindi verificare che sia dello stesso tipo potrebbe anche essere indesiderabile.
gotgenes,

12
Suggerirei di restituire NotImplemented se i tipi sono diversi, delegando il confronto a rhs.
max

4
Il confronto @max non è necessariamente eseguito dal lato sinistro (LHS) al lato destro (RHS), quindi da RHS a LHS; vedi stackoverflow.com/a/12984987/38140 . Tuttavia, tornare NotImplementedcome suggerisci causerà sempre superclass.__eq__(subclass), che è il comportamento desiderato.
gotgenes,

4
Se hai un sacco di membri e non ci sono molte copie di oggetti in giro, di solito è bene aggiungere un test di identità iniziale if other is self. Ciò evita il confronto più lungo del dizionario e può essere un grande risparmio quando gli oggetti vengono utilizzati come chiavi del dizionario.
Dane White,

2
E non dimenticare di implementare__hash__()
Dane White il

161

Il modo in cui descrivi è il modo in cui l'ho sempre fatto. Dal momento che è totalmente generico, puoi sempre suddividere quella funzionalità in una classe mixin ed ereditarla in classi in cui desideri quella funzionalità.

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

6
+1: modello di strategia per consentire una facile sostituzione in sottoclassi.
S.Lott

3
isinstance fa schifo. Perché controllarlo? Perché non solo sé .__ dict__ == altro .__ dict__?
nosklo,

3
@nosklo: Non capisco .. e se due oggetti di classi completamente non correlate avessero gli stessi attributi?
massimo

1
Pensavo che Nokslo avesse suggerito di saltare Isinstance. In tal caso non sai più se otherappartiene a una sottoclasse di self.__class__.
massimo

10
Un altro problema con il __dict__confronto è cosa succede se si dispone di un attributo che non si desidera considerare nella definizione di uguaglianza (ad esempio un ID oggetto univoco o metadati come un timbro creato nel tempo).
Adam Parkin,

14

Non è una risposta diretta, ma mi è sembrato abbastanza pertinente da essere affrontato in quanto consente di risparmiare un po 'di tedio verboso in alcune occasioni. Taglia direttamente dai documenti ...


functools.total_ordering (CLS)

Data una classe che definisce uno o più ricchi metodi di ordinamento del confronto, questo decoratore di classe fornisce il resto. Ciò semplifica lo sforzo necessario per specificare tutte le possibili operazioni di confronto avanzate:

La classe deve definire uno dei __lt__(), __le__(), __gt__(), o __ge__(). Inoltre, la classe dovrebbe fornire un __eq__()metodo.

Novità nella versione 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()))

1
Tuttavia total_ordering ha insidie ​​sottili: regebro.wordpress.com/2010/12/13/… . Sii consapevole!
Mr_and_Mrs_D

8

Non è necessario ignorare entrambi __eq__ e __ne__si può solo eseguire l'override, __cmp__ma ciò influirà sul risultato di ==,! ==, <,> e così via.

istest per l'identità dell'oggetto. Ciò significa che a isb sarà Truenel caso in cui aeb mantengano entrambi il riferimento allo stesso oggetto. In Python hai sempre un riferimento a un oggetto in una variabile e non l'oggetto reale, quindi essenzialmente per a is b per essere vero gli oggetti in essi contenuti dovrebbero trovarsi nella stessa posizione di memoria. Come e soprattutto perché dovresti ignorare questo comportamento?

Modifica: non sapevo che __cmp__fosse rimosso da Python 3, quindi evitatelo.


Perché a volte hai una diversa definizione di uguaglianza per i tuoi oggetti.
Ed S.

l'operatore is ti dà la risposta degli interpreti all'identità dell'oggetto, ma sei ancora libero di esprimere la tua visione sull'uguaglianza sovrascrivendo cmp
Vasil

7
In Python 3, "La funzione cmp () è sparita e il metodo speciale __cmp __ () non è più supportato." is.gd/aeGv
gotgenes il


2

Penso che i due termini che stai cercando siano uguaglianza (==) e identità (è). Per esempio:

>>> 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

1
Forse, tranne per il fatto che si può creare una classe che confronta solo i primi due elementi in due elenchi e, se tali elementi sono uguali, viene valutato su Vero. Questa è equivalenza, penso, non uguaglianza. Perfettamente valido in eq , ancora.
gotgenes,

Concordo, tuttavia, che "is" è un test di identità.
gotgenes,

1

Il test 'is' verificherà l'identità usando la funzione incorporata 'id ()' che essenzialmente restituisce l'indirizzo di memoria dell'oggetto e quindi non è sovraccaricabile.

Tuttavia, nel caso di test dell'uguaglianza di una classe, probabilmente si desidera essere un po 'più severi nei confronti dei test e confrontare solo gli attributi dei dati nella propria 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

Questo codice confronta solo i membri di dati non funzionali della tua classe, oltre a saltare qualsiasi cosa privata che è generalmente ciò che desideri. Nel caso di Plain Old Python Objects ho una classe base che implementa __init__, __str__, __repr__ e __eq__, quindi i miei oggetti POPO non portano il peso di tutta quella logica extra (e nella maggior parte dei casi identica).


È un po 'pignolo, ma' is 'testa usando id () solo se non hai definito la tua funzione membro is_ () (2.3+). [ docs.python.org/library/operator.html]
spenthil,

Suppongo che per "override" si intenda effettivamente patch di scimmie sul modulo operatore. In questo caso la tua affermazione non è del tutto accurata. Il modulo operatori è fornito per comodità e l'override di tali metodi non influisce sul comportamento dell'operatore "is". Un confronto che utilizza "is" utilizza sempre l'id () di un oggetto per il confronto, questo comportamento non può essere ignorato. Anche una funzione membro is_ non ha alcun effetto sul confronto.
mcrute il

mcrute - Ho parlato troppo presto (e in modo errato), hai assolutamente ragione.
spenthil,

Questa è una soluzione molto bella, specialmente quando __eq__verrà dichiarato in CommonEqualityMixin(vedi l'altra risposta). L'ho trovato particolarmente utile quando si confrontavano istanze di classi derivate da Base in SQLAlchemy. Per non confrontare _sa_instance_stateho cambiato key.startswith("__")):in key.startswith("_")):. Avevo anche alcuni riferimenti arretrati e la risposta di Algorias ha generato una ricorsione infinita. Quindi ho chiamato tutte le backreferenze a partire da in '_'modo che vengano saltate anche durante il confronto. NOTA: in Python 3.x passare iteritems()a items().
Wookie88,

@mcrute Di solito, __dict__di un'istanza non ha nulla che inizia con a __meno che non sia stato definito dall'utente. Cose come __class__, __init__ecc. Non sono nelle istanze __dict__, ma piuttosto nella sua classe " __dict__. OTOH, gli attributi privati ​​possono facilmente iniziare con __e probabilmente dovrebbero essere usati per __eq__. Puoi chiarire che cosa esattamente stavi cercando di evitare quando __saltavi gli attributi-prefissati?
massimo

1

Invece di utilizzare la sottoclasse / mixin, mi piace usare un decoratore di classe generico

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

Uso:

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

a = Number(1)
b = Number(1)
assert a == b

0

Questo incorpora i commenti sulla risposta di Algorias e confronta gli oggetti con un singolo attributo perché non mi interessa l'intero dict. hasattr(other, "id")deve essere vero, ma so che è perché l'ho impostato nel costruttore.

def __eq__(self, other):
    if other is self:
        return True

    if type(other) is not type(self):
        # delegate to superclass
        return NotImplemented

    return other.id == self.id
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.