Python, dovrei implementare l'operatore __ne __ () basato su __eq__?


98

Ho una classe in cui desidero sovrascrivere l' __eq__()operatore. Sembra logico che io debba sovrascrivere anche l' __ne__()operatore, ma ha senso implementare in __ne__base a __eq__come tale?

class A:
    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return not self.__eq__(other)

O c'è qualcosa che mi manca nel modo in cui Python usa questi operatori che lo rende non una buona idea?

Risposte:


57

Sì, va benissimo. La documentazione , infatti, ti esorta a definire __ne__quando definisci __eq__:

Non ci sono relazioni implicite tra gli operatori di confronto. La verità di x==ynon implica che x!=y sia falso. Di conseguenza, quando si definisce __eq__(), si dovrebbe anche definire in __ne__()modo che gli operatori si comportino come previsto.

In molti casi (come questo), sarà semplice come negare il risultato di __eq__, ma non sempre.


12
questa è la risposta giusta (qui sotto, di @ aaron-hall). La documentazione che hai citato non ti incoraggia a implementare l' __ne__uso __eq__, ma solo a implementarlo.
guyarad

2
@ guyarad: In realtà, la risposta di Aaron è ancora leggermente sbagliata grazie alla mancata delega; invece di considerare un NotImplementedritorno da un lato come uno spunto a cui delegare __ne__dall'altro lato, not self == otherè (supponendo che l'operando __eq__non sappia come confrontare l'altro operando) delegando implicitamente a __eq__dall'altro lato, quindi invertendolo. Per i tipi strani, ad esempio i campi di SQLAlchemy ORM, questo causa problemi .
ShadowRanger

1
La critica di ShadowRanger si applicherebbe solo a casi molto patologici (IMHO) ed è completamente affrontata nella mia risposta di seguito.
Aaron Hall

1
Le documentazioni più recenti (almeno per la 3.7, potrebbero essere anche precedenti) __ne__delegano automaticamente a __eq__e la citazione in questa risposta non esiste più nei documenti. In conclusione, è perfettamente pitonico implementare __eq__e lasciare __ne__delegare.
bluesummers

132

Python, dovrei implementare un __ne__()operatore basato su __eq__?

Risposta breve: non implementarlo, ma se devi, usa ==, no__eq__

In Python 3, !=è la negazione di ==default, quindi non ti viene nemmeno richiesto di scrivere a __ne__, e la documentazione non è più supponente di scriverne uno.

In generale, per il codice Python 3 solo, non scriverne uno a meno che non sia necessario oscurare l'implementazione genitore, ad esempio per un oggetto incorporato.

Cioè, tieni presente il commento di Raymond Hettinger :

Il __ne__metodo segue automaticamente __eq__solo se __ne__non è già definito in una superclasse. Quindi, se stai ereditando da un builtin, è meglio sovrascriverli entrambi.

Se hai bisogno che il tuo codice funzioni in Python 2, segui i consigli per Python 2 e funzionerà perfettamente in Python 3.

In Python 2, Python stesso non implementa automaticamente alcuna operazione in termini di un'altra, quindi dovresti definire il __ne__in termini di ==invece di __eq__. PER ESEMPIO

class A(object):
    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return not self == other # NOT `return not self.__eq__(other)`

Vedi la prova

  • __ne__()operatore di implementazione basato su __eq__e
  • non implementato affatto __ne__in Python 2

fornisce un comportamento non corretto nella dimostrazione di seguito.

Risposta lunga

La documentazione per Python 2 dice:

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 comportino come previsto.

Quindi ciò significa che se definiamo __ne__in termini di inverso di __eq__, possiamo ottenere un comportamento coerente.

Questa sezione della documentazione è stata aggiornata per Python 3:

Per impostazione predefinita, __ne__()delega __eq__()e inverte il risultato a meno che non lo sia NotImplemented.

e nella sezione "novità" , vediamo che questo comportamento è cambiato:

  • !=ora restituisce l'opposto di ==, a meno che non ==ritorni NotImplemented.

Per l'implementazione __ne__, preferiamo utilizzare l' ==operatore invece di utilizzare __eq__direttamente il metodo in modo che se self.__eq__(other)di una sottoclasse restituisce NotImplementedil tipo verificato, Python controllerà opportunamente other.__eq__(self) Dalla documentazione :

L' NotImplementedoggetto

Questo tipo ha un unico valore. C'è un singolo oggetto con questo valore. A questo oggetto si accede tramite il nome predefinito NotImplemented. I metodi numerici e i metodi di confronto avanzato possono restituire questo valore se non implementano l'operazione per gli operandi forniti. (L'interprete proverà quindi l'operazione riflessa, o qualche altro fallback, a seconda dell'operatore.) Il suo valore di verità è vero.

Quando somministrato un ricco operatore di confronto, se non sono dello stesso tipo, controlla se il Python otherè un sottotipo, e se ha tale operatore definito, si utilizza il othermetodo di 's primo (inversa per <, <=, >=e >). Se NotImplementedviene restituito, quindi utilizza il metodo del contrario. (Esso non controllare lo stesso metodo per due volte.) Uso del ==operatore consente questa logica avvenire.


Aspettative

Semanticamente, dovresti implementare __ne__in termini di controllo dell'uguaglianza perché gli utenti della tua classe si aspetteranno che le seguenti funzioni siano equivalenti per tutte le istanze di A .:

def negation_of_equals(inst1, inst2):
    """always should return same as not_equals(inst1, inst2)"""
    return not inst1 == inst2

def not_equals(inst1, inst2):
    """always should return same as negation_of_equals(inst1, inst2)"""
    return inst1 != inst2

Cioè, entrambe le funzioni precedenti dovrebbero sempre restituire lo stesso risultato. Ma questo dipende dal programmatore.

Dimostrazione di comportamento imprevisto durante la definizione __ne__basata su __eq__:

Prima la configurazione:

class BaseEquatable(object):
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return isinstance(other, BaseEquatable) and self.x == other.x

class ComparableWrong(BaseEquatable):
    def __ne__(self, other):
        return not self.__eq__(other)

class ComparableRight(BaseEquatable):
    def __ne__(self, other):
        return not self == other

class EqMixin(object):
    def __eq__(self, other):
        """override Base __eq__ & bounce to other for __eq__, e.g. 
        if issubclass(type(self), type(other)): # True in this example
        """
        return NotImplemented

class ChildComparableWrong(EqMixin, ComparableWrong):
    """__ne__ the wrong way (__eq__ directly)"""

class ChildComparableRight(EqMixin, ComparableRight):
    """__ne__ the right way (uses ==)"""

class ChildComparablePy3(EqMixin, BaseEquatable):
    """No __ne__, only right in Python 3."""

Crea istanze non equivalenti:

right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)

Comportamento atteso:

(Nota: mentre ogni seconda asserzione di ciascuna delle seguenti è equivalente e quindi logicamente ridondante a quella precedente, le includo per dimostrare che l' ordine non ha importanza quando una è una sottoclasse dell'altra. )

Queste istanze sono state __ne__implementate con ==:

assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1

Anche queste istanze, testate con Python 3, funzionano correttamente:

assert not right_py3_1 == right_py3_2
assert not right_py3_2 == right_py3_1
assert right_py3_1 != right_py3_2
assert right_py3_2 != right_py3_1

E ricorda che questi sono stati __ne__implementati con __eq__- sebbene questo sia il comportamento previsto, l'implementazione non è corretta:

assert not wrong1 == wrong2         # These are contradicted by the
assert not wrong2 == wrong1         # below unexpected behavior!

Comportamento inaspettato:

Si noti che questo confronto contraddice i confronti sopra ( not wrong1 == wrong2).

>>> assert wrong1 != wrong2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

e,

>>> assert wrong2 != wrong1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

Non saltare __ne__in Python 2

Per provare che non dovresti saltare l'implementazione __ne__in Python 2, vedi questi oggetti equivalenti:

>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True

Il risultato sopra dovrebbe essere False!

Sorgente Python 3

L'implementazione predefinita di CPython per __ne__è typeobject.cinobject_richcompare :

case Py_NE:
    /* By default, __ne__() delegates to __eq__() and inverts the result,
       unless the latter returns NotImplemented. */
    if (Py_TYPE(self)->tp_richcompare == NULL) {
        res = Py_NotImplemented;
        Py_INCREF(res);
        break;
    }
    res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
    if (res != NULL && res != Py_NotImplemented) {
        int ok = PyObject_IsTrue(res);
        Py_DECREF(res);
        if (ok < 0)
            res = NULL;
        else {
            if (ok)
                res = Py_False;
            else
                res = Py_True;
            Py_INCREF(res);
        }
    }
    break;

Ma l'impostazione predefinita __ne__utilizza __eq__?

I __ne__dettagli di implementazione predefiniti di Python 3 a livello C vengono usati __eq__perché il livello più alto ==( PyObject_RichCompare ) sarebbe meno efficiente e quindi deve anche gestire NotImplemented.

Se __eq__è implementato correttamente, anche la negazione di ==è corretta e ci consente di evitare dettagli di implementazione di basso livello nel nostro __ne__.

Utilizzando ==ci permette di mantenere la nostra logica di basso livello in un luogo, ed evitare di affrontare NotImplementedin __ne__.

Si potrebbe erroneamente presumere che ==possa tornare NotImplemented.

In realtà utilizza la stessa logica dell'implementazione predefinita di __eq__, che controlla l'identità (vedere do_richcompare e le nostre prove di seguito)

class Foo:
    def __ne__(self, other):
        return NotImplemented
    __eq__ = __ne__

f = Foo()
f2 = Foo()

E i confronti:

>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True

Prestazione

Non credermi sulla parola, vediamo cosa è più performante:

class CLevel:
    "Use default logic programmed in C"

class HighLevelPython:
    def __ne__(self, other):
        return not self == other

class LowLevelPython:
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

def c_level():
    cl = CLevel()
    return lambda: cl != cl

def high_level_python():
    hlp = HighLevelPython()
    return lambda: hlp != hlp

def low_level_python():
    llp = LowLevelPython()
    return lambda: llp != llp

Penso che questi numeri di prestazioni parlino da soli:

>>> import timeit
>>> min(timeit.repeat(c_level()))
0.09377292497083545
>>> min(timeit.repeat(high_level_python()))
0.2654011140111834
>>> min(timeit.repeat(low_level_python()))
0.3378178110579029

Questo ha senso se si considera che low_level_pythonsta facendo logica in Python che altrimenti verrebbe gestita a livello C.

Risposta ad alcuni critici

Un altro risponditore scrive:

L'implementazione not self == otherdi Aaron Hall del __ne__metodo non è corretta in quanto non può mai restituire NotImplemented( not NotImplementedè False) e quindi il __ne__metodo che ha la priorità non può mai ripiegare sul __ne__metodo che non ha la priorità.

Non essere __ne__mai tornato NotImplementednon lo rende errato. Invece, gestiamo la priorità con NotImplementedtramite il controllo dell'uguaglianza con ==. Supponendo che ==sia implementato correttamente, abbiamo finito.

not self == otherera l'implementazione predefinita di Python 3 del __ne__metodo, ma era un bug ed è stato corretto in Python 3.4 a gennaio 2015, come notato da ShadowRanger (vedi problema # 21408).

Bene, spieghiamo questo.

Come notato in precedenza, Python 3 gestisce per impostazione predefinita __ne__controllando prima se self.__eq__(other)restituisce NotImplemented(un singleton), che dovrebbe essere controllato con ise restituito in caso affermativo, altrimenti dovrebbe restituire l'inverso. Ecco quella logica scritta come un mix di classi:

class CStyle__ne__:
    """Mixin that provides __ne__ functionality equivalent to 
    the builtin functionality
    """
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

Ciò è necessario per la correttezza dell'API Python di livello C ed è stato introdotto in Python 3, rendendo

ridondante. Tutti i __ne__metodi pertinenti sono stati rimossi, compresi quelli che implementano il proprio controllo e quelli che delegano __eq__direttamente o tramite ==- ed ==era il modo più comune per farlo.

La simmetria è importante?

Nostro critico persistente fornisce un esempio patologico per fare il caso per la gestione NotImplementedin __ne__, valorizzando simmetria soprattutto. Facciamo Steel-Man l'argomento con un chiaro esempio:

class B:
    """
    this class has no __eq__ implementation, but asserts 
    any instance is not equal to any other object
    """
    def __ne__(self, other):
        return True

class A:
    "This class asserts instances are equivalent to all other objects"
    def __eq__(self, other):
        return True

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, False, True)

Quindi, con questa logica, per mantenere la simmetria, dobbiamo scrivere il complicato __ne__, indipendentemente dalla versione di Python.

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return True
    def __ne__(self, other):
        result = other.__eq__(self)
        if result is NotImplemented:
            return NotImplemented
        return not result

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, True, True)

Apparentemente non dovremmo tenere conto del fatto che questi casi sono uguali e non uguali.

Propongo che la simmetria sia meno importante della presunzione di codice ragionevole e seguendo i consigli della documentazione.

Tuttavia, se A avesse un'implementazione ragionevole di __eq__, allora potremmo ancora seguire la mia direzione qui e avremmo ancora simmetria:

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return False         # <- this boolean changed... 

>>> A() == B(), B() == A(), A() != B(), B() != A()
(False, False, True, True)

Conclusione

Per il codice compatibile con Python 2, usa ==per implementare __ne__. È di più:

  • corretta
  • semplice
  • performante

Solo in Python 3, usa la negazione di basso livello sul livello C: è ancora più semplice e performante (sebbene il programmatore sia responsabile di determinare che sia corretto ).

Ancora una volta, non scrivere logica di basso livello in Python di alto livello.


3
Ottimi esempi! Parte della sorpresa è che l'ordine degli operandi non ha alcuna importanza , a differenza di alcuni metodi magici con i loro riflessi "sul lato destro". Per ripetere la parte che mi è sfuggita (e che mi è costato molto tempo): Si prova per primo il ricco metodo di confronto della sottoclasse , indipendentemente dal fatto che il codice abbia la superclasse o la sottoclasse a sinistra dell'operatore. Questo è il motivo per cui sei a1 != c2tornato False--- non è stato eseguito a1.__ne__, ma c2.__ne__, il che ha negato il metodo di missaggio __eq__ . Dal momento che NotImplementedè vero, lo not NotImplementedè False.
Kevin J. Chase

2
I tuoi aggiornamenti recenti dimostrano con successo il vantaggio in termini di prestazioni di not (self == other), ma nessuno sostiene che non sia veloce (beh, più veloce di qualsiasi altra opzione su Py2 comunque). Il problema è che in alcuni casi è sbagliato ; Lo stesso Python lo faceva not (self == other), ma è cambiato perché non era corretto in presenza di sottoclassi arbitrarie . Il più veloce alla risposta sbagliata è ancora sbagliato .
ShadowRanger

1
L'esempio specifico è davvero poco importante. Il problema è che, nella tua implementazione, il comportamento dei tuoi __ne__delegati a __eq__(di entrambe le parti se necessario), ma non ricade mai__ne__ sull'altro lato anche quando entrambi __eq__"si arrendono". I __ne__delegati corretti al proprio __eq__ , ma se ritorna NotImplemented, ricade per andare dall'altra parte __ne__, piuttosto che invertire quella dell'altra parte __eq__(poiché l'altra parte potrebbe non aver esplicitamente optato per delegare a __eq__, e non dovresti prendere quella decisione per questo).
ShadowRanger

1
@AaronHall: Riesaminando questo oggi, non penso che la tua implementazione sia problematica per le sottoclassi normalmente (sarebbe estremamente contorto farlo rompere, e la sottoclasse, presumendo che abbia piena conoscenza del genitore, dovrebbe essere in grado di evitarlo ). Ma ho appena fornito un esempio non contorto nella mia risposta. Il caso non patologico è l'ORM di SQLAlchemy, dove né __eq____ne__restituisce Trueo False, ma piuttosto un oggetto proxy (che sembra essere "veritiero"). L'implementazione non corretta __ne__significa che l'ordine è importante per il confronto (si ottiene solo un proxy in un ordine).
ShadowRanger

1
Per essere chiari, nel 99% (o forse nel 99,999%) dei casi, la tua soluzione va bene e (ovviamente) più veloce. Ma dal momento che non hai il controllo sui casi in cui non va bene, come scrittore di librerie il cui codice può essere usato da altri (leggi: qualsiasi cosa tranne semplici script e moduli una tantum esclusivamente per uso personale), devi utilizzare l'implementazione corretta per aderire al contratto generale per il sovraccarico degli operatori e lavorare con qualsiasi altro codice che potresti incontrare. Fortunatamente, su Py3, niente di tutto questo ha importanza, dal momento che puoi omettere __ne__completamente. Tra un anno, Py2 sarà morto e lo ignoriamo. :-)
ShadowRanger

10

Solo per la cronaca, un portatile Py2 / Py3 canonicamente corretto e incrociato __ne__sarebbe simile a:

import sys

class ...:
    ...
    def __eq__(self, other):
        ...

    if sys.version_info[0] == 2:
        def __ne__(self, other):
            equal = self.__eq__(other)
            return equal if equal is NotImplemented else not equal

Funziona con qualsiasi cosa __eq__tu possa definire:

  • A differenza not (self == other), non interferisce in alcuni casi fastidiosi / complessi che coinvolgono confronti in cui una delle classi coinvolte non implica che il risultato di __ne__è lo stesso del risultato di noton __eq__(ad esempio ORM di SQLAlchemy, dove entrambi __eq__e __ne__restituiscono oggetti proxy speciali, no Trueo False, e il tentativo notdi __eq__restituire il risultato di False, piuttosto che l'oggetto proxy corretto).
  • Diversamente not self.__eq__(other), questo delega correttamente alla __ne__dell'altra istanza quando self.__eq__ritorna NotImplemented( not self.__eq__(other)sarebbe più sbagliato, perché NotImplementedè vero, quindi quando __eq__non sapevo come eseguire il confronto, __ne__tornerebbe False, implicando che i due oggetti erano uguali quando in realtà l'unico l'oggetto chiesto non aveva idea, il che implicherebbe un valore predefinito di non uguale)

Se __eq__non usi i NotImplementedritorni, funziona (con un sovraccarico senza senso), se lo usa a NotImplementedvolte, lo gestisce correttamente. E il controllo della versione di Python significa che se la classe è import-ed in Python 3, __ne__viene lasciata indefinita, consentendo l' __ne__implementazione di fallback nativa ed efficiente di Python (una versione C della precedente) di prendere il sopravvento.


Perché è necessario

Regole di sovraccarico di Python

La spiegazione del motivo per cui lo fai invece di altre soluzioni è alquanto arcana. Python ha un paio di regole generali sul sovraccarico degli operatori e in particolare sugli operatori di confronto:

  1. (Si applica a tutti gli operatori) Durante l'esecuzione LHS OP RHS, prova LHS.__op__(RHS)e, se ritorna NotImplemented, prova RHS.__rop__(LHS). Eccezione: se RHSè una sottoclasse della LHSclasse di, eseguire RHS.__rop__(LHS) prima il test . Nel caso degli operatori di confronto, __eq__e __ne__sono i loro "rop" (quindi l'ordine di test per __ne__è LHS.__ne__(RHS), quindi RHS.__ne__(LHS), invertito se RHSè una sottoclasse della LHSclasse s)
  2. A parte l'idea dell'operatore "scambiato", non esiste alcuna relazione implicita tra gli operatori. Anche per esempio della stessa classe, la LHS.__eq__(RHS)restituzione Truenon implica la LHS.__ne__(RHS)restituzione False(infatti, gli operatori non sono nemmeno tenuti a restituire valori booleani; ORM come SQLAlchemy intenzionalmente non lo fanno, consentendo una sintassi di query più espressiva). A partire da Python 3, l' __ne__implementazione predefinita si comporta in questo modo, ma non è contrattuale; puoi eseguire l'override __ne__in modi che non sono strettamente opposti di __eq__.

Come questo si applica al sovraccarico dei comparatori

Quindi, quando sovraccarichi un operatore, hai due lavori:

  1. Se sai come implementare l'operazione da solo, fallo, usando solo la tua conoscenza di come fare il confronto (non delegare mai, implicitamente o esplicitamente, all'altro lato dell'operazione; farlo rischi di scorrettezza e / o ricorsione infinita, a seconda di come lo fai)
  2. Se non sai come implementare l'operazione da solo, torna sempreNotImplemented , così Python può delegare l'implementazione dell'altro operando

Il problema con not self.__eq__(other)

def __ne__(self, other):
    return not self.__eq__(other)

non delega mai all'altro lato (e non è corretto se __eq__restituisce correttamente NotImplemented). Quando self.__eq__(other)ritorna NotImplemented(che è "veritiero"), tu ritorni silenziosamente False, quindi A() != something_A_knows_nothing_aboutritorna False, quando avrebbe dovuto controllare se something_A_knows_nothing_aboutsapeva come confrontare le istanze di A, e se non lo fa, dovrebbe essere restituito True(poiché se nessuna delle parti sa come rispetto agli altri, sono considerati non uguali tra loro). Se A.__eq__è implementato in modo errato (restituendo Falseinvece di NotImplementedquando non riconosce l'altro lato), allora è "corretto" dal Apunto di vista di, restituendo True(poiché Anon pensa che sia uguale, quindi non è uguale), ma potrebbe essere sbagliato dasomething_A_knows_nothing_aboutLa prospettiva di, visto che non l'ha mai nemmeno chiesto something_A_knows_nothing_about; A() != something_A_knows_nothing_aboutfinisce True, ma something_A_knows_nothing_about != A()potrebbe False, o qualsiasi altro valore di ritorno.

Il problema con not self == other

def __ne__(self, other):
    return not self == other

è più sottile. Sarà corretto per il 99% delle classi, comprese tutte le classi per le quali __ne__è l'inverso logico di __eq__. Ma not self == otherinfrange entrambe le regole sopra menzionate, il che significa che per le classi in cui __ne__ non è l'inverso logico di __eq__, i risultati sono ancora una volta non simmetrici, perché a uno degli operandi non viene mai chiesto se può essere implementato __ne__, anche se l'altro operando non può. L'esempio più semplice è una classe strano che i rendimenti Falseper tutti i confronti, in modo A() == Incomparable()ed A() != Incomparable()entrambi di ritorno False. Con una corretta implementazione di A.__ne__(che ritorna NotImplementedquando non sa come fare il confronto), la relazione è simmetrica; A() != Incomparable()eIncomparable() != A()concordare il risultato (perché nel primo caso A.__ne__ritorna NotImplemented, poi Incomparable.__ne__ritorna False, mentre nel secondo Incomparable.__ne__ritorna Falsedirettamente). Ma quando A.__ne__è implementato come return not self == other, A() != Incomparable()ritorna True(perché A.__eq__restituisce, non NotImplemented, quindi Incomparable.__eq__ritorna Falsee lo A.__ne__inverte in True), mentre Incomparable() != A()ritornaFalse.

Puoi vedere un esempio di questo in azione qui .

Ovviamente, una classe che ritorna sempre Falseper entrambi __eq__ed __ne__è un po 'strana. Ma come accennato prima, __eq__e __ne__non c'è nemmeno bisogno di tornare True/ False; l'ORM SQLAlchemy ha classi con comparatori che restituiscono uno speciale oggetto proxy per la creazione di query, per niente True/ False(sono "veritieri" se valutati in un contesto booleano, ma non dovrebbero mai essere valutati in tale contesto).

Omettendo di sovraccarico __ne__correttamente, si romperà le classi di questo tipo, come il codice:

 results = session.query(MyTable).filter(MyTable.fieldname != MyClassWithBadNE())

funzionerà (supponendo che SQLAlchemy sappia come inserire MyClassWithBadNEin una stringa SQL; questo può essere fatto con adattatori di tipo senza MyClassWithBadNEdover cooperare affatto), passando l'oggetto proxy previsto a filter, mentre:

 results = session.query(MyTable).filter(MyClassWithBadNE() != MyTable.fieldname)

finirà per passare filterun semplice False, perché self == otherrestituisce un oggetto proxy e not self == otherconverte semplicemente l'oggetto proxy vero in False. Si spera che filtergeneri un'eccezione quando vengono gestiti argomenti non validi come False. Mentre sono sicuro che molti sosterranno che MyTable.fieldname dovrebbe essere costantemente sul lato sinistro del confronto, resta il fatto che non vi è alcun motivo programmatico per imporlo nel caso generale, e un generico corretto __ne__funzionerà in entrambi i casi, mentre return not self == otherfunziona solo in una disposizione.


1
L'unica risposta corretta, completa e onesta (mi dispiace @AaronHall). Questa dovrebbe essere la risposta accettata.
Maggyero

4

Risposta breve: sì (ma leggi la documentazione per farlo bene)

L'implementazione del __ne__metodo di ShadowRanger è quella corretta (e sembra essere l'implementazione predefinita del __ne__metodo a partire da Python 3.4):

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

Perché? Perché mantiene una importante proprietà matematica, la simmetria del !=operatore. Questo operatore è binario, quindi il suo risultato dovrebbe dipendere dal tipo dinamico di entrambi gli operandi, non solo di uno. Questo viene implementato tramite doppio invio per linguaggi di programmazione che consentono più invii (come Julia ). In Python, che consente solo l'invio singolo, il doppio invio viene simulato per metodi numerici e metodi di confronto ricchi restituendo il valore NotImplementednei metodi di implementazione che non supportano il tipo dell'altro operando; l'interprete proverà quindi il metodo riflesso dell'altro operando.

L'implementazione not self == otherdel __ne__metodo di Aaron Hall non è corretta poiché rimuove la simmetria !=dell'operatore. In effetti, non può mai tornare NotImplemented( not NotImplementedè False) e quindi il __ne__metodo con priorità più alta non può mai ricorrere al __ne__metodo con priorità inferiore. not self == otherera l'implementazione predefinita di Python 3 del __ne__metodo, ma si trattava di un bug che è stato corretto in Python 3.4 a gennaio 2015, come notato da ShadowRanger (vedi problema # 21408 ).

Implementazione degli operatori di confronto

Il Python Language Reference per Python 3 afferma nel suo capitolo III Modello di dati :

object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)

Questi sono i cosiddetti metodi di "confronto ricco". La corrispondenza tra i simboli degli operatori e i nomi dei metodi è la seguente: x<ychiamate x.__lt__(y), x<=ychiamate x.__le__(y), x==ychiamate x.__eq__(y), x!=ychiamate x.__ne__(y), x>ychiamate x.__gt__(y)e x>=y chiamate x.__ge__(y).

Un ricco metodo di confronto può restituire il singleton NotImplementedse non implementa l'operazione per una data coppia di argomenti.

Non esistono versioni con argomenti scambiati di questi metodi (da utilizzare quando l'argomento a sinistra non supporta l'operazione ma l'argomento a destra sì); piuttosto, __lt__()e __gt__()sono il riflesso l'uno dell'altro, __le__()e __ge__()sono il riflesso l'uno dell'altro, __eq__()e __ne__()sono il proprio riflesso. Se gli operandi sono di tipi diversi e il tipo dell'operando destro è una sottoclasse diretta o indiretta del tipo dell'operando sinistro, il metodo riflesso dell'operando destro ha la priorità, altrimenti il ​​metodo dell'operando sinistro ha la priorità. La sottoclasse virtuale non è considerata.

Tradurre questo in codice Python dà (usando operator_eqper ==, operator_neper !=, operator_ltper <, operator_gtper >, operator_leper <=e operator_geper >=):

def operator_eq(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__eq__(left)

        if result is NotImplemented:
            result = left.__eq__(right)
    else:
        result = left.__eq__(right)

        if result is NotImplemented:
            result = right.__eq__(left)

    if result is NotImplemented:
        result = left is right

    return result


def operator_ne(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ne__(left)

        if result is NotImplemented:
            result = left.__ne__(right)
    else:
        result = left.__ne__(right)

        if result is NotImplemented:
            result = right.__ne__(left)

    if result is NotImplemented:
        result = left is not right

    return result


def operator_lt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__gt__(left)

        if result is NotImplemented:
            result = left.__lt__(right)
    else:
        result = left.__lt__(right)

        if result is NotImplemented:
            result = right.__gt__(left)

    if result is NotImplemented:
        raise TypeError(f"'<' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_gt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__lt__(left)

        if result is NotImplemented:
            result = left.__gt__(right)
    else:
        result = left.__gt__(right)

        if result is NotImplemented:
            result = right.__lt__(left)

    if result is NotImplemented:
        raise TypeError(f"'>' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_le(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ge__(left)

        if result is NotImplemented:
            result = left.__le__(right)
    else:
        result = left.__le__(right)

        if result is NotImplemented:
            result = right.__ge__(left)

    if result is NotImplemented:
        raise TypeError(f"'<=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result


def operator_ge(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__le__(left)

        if result is NotImplemented:
            result = left.__ge__(right)
    else:
        result = left.__ge__(right)

        if result is NotImplemented:
            result = right.__le__(left)

    if result is NotImplemented:
        raise TypeError(f"'>=' not supported between instances of '{type(left).__name__}' and '{type(right).__name__}'")

    return result

Implementazione predefinita dei metodi di confronto

La documentazione aggiunge:

Per impostazione predefinita, __ne__()delega __eq__()e inverte il risultato a meno che non lo sia NotImplemented. Non ci sono altre relazioni implicite tra gli operatori di confronto, ad esempio, la verità di (x<y or x==y)non implica x<=y.

L'implementazione predefinita dei metodi di confronto ( __eq__, __ne__, __lt__, __gt__, __le__e __ge__) può quindi essere data da:

def __eq__(self, other):
    return NotImplemented

def __ne__(self, other):
    result = self.__eq__(other)

    if result is not NotImplemented:
        return not result

    return NotImplemented

def __lt__(self, other):
    return NotImplemented

def __gt__(self, other):
    return NotImplemented

def __le__(self, other):
    return NotImplemented

def __ge__(self, other):
    return NotImplemented

Quindi questa è la corretta implementazione del __ne__metodo. E non restituisce sempre l'inverso del __eq__metodo, perché quando il __eq__metodo ritorna NotImplemented, la sua inversa not NotImplementedè False(come bool(NotImplemented)è True) invece del desiderato NotImplemented.

Implementazioni errate di __ne__

Come Aaron Hall ha dimostrato sopra, not self.__eq__(other)non è l'implementazione predefinita del __ne__metodo. Ma non lo è not self == other. Quest'ultimo è dimostrato di seguito confrontando il comportamento dell'implementazione predefinita con il comportamento not self == otherdell'implementazione in due casi:

  • il __eq__metodo ritorna NotImplemented;
  • il __eq__metodo restituisce un valore diverso da NotImplemented.

Implementazione predefinita

Vediamo cosa succede quando il A.__ne__metodo utilizza l'implementazione predefinita e il A.__eq__metodo restituisce NotImplemented:

class A:
    pass


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) == "B.__ne__"
  1. !=chiamate A.__ne__.
  2. A.__ne__chiamate A.__eq__.
  3. A.__eq__ritorna NotImplemented.
  4. !=chiamate B.__ne__.
  5. B.__ne__ritorna "B.__ne__".

Ciò mostra che quando il A.__eq__metodo ritorna NotImplemented, il A.__ne__metodo ricade sul B.__ne__metodo.

Vediamo ora cosa succede quando il A.__ne__metodo utilizza l'implementazione predefinita e il A.__eq__metodo restituisce un valore diverso da NotImplemented:

class A:

    def __eq__(self, other):
        return True


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. !=chiamate A.__ne__.
  2. A.__ne__chiamate A.__eq__.
  3. A.__eq__ritorna True.
  4. !=restituisce not True, cioè False.

Ciò mostra che in questo caso, il A.__ne__metodo restituisce l'inverso del A.__eq__metodo. Quindi il __ne__metodo si comporta come pubblicizzato nella documentazione.

Ignorare l'implementazione predefinita del A.__ne__metodo con l'implementazione corretta data sopra produce gli stessi risultati.

not self == other implementazione

Vediamo cosa succede quando si sovrascrive l'implementazione predefinita del A.__ne__metodo con l' not self == otherimplementazione e il A.__eq__metodo restituisce NotImplemented:

class A:

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is True
  1. !=chiamate A.__ne__.
  2. A.__ne__chiamate ==.
  3. ==chiamate A.__eq__.
  4. A.__eq__ritorna NotImplemented.
  5. ==chiamate B.__eq__.
  6. B.__eq__ritorna NotImplemented.
  7. ==restituisce A() is B(), cioè False.
  8. A.__ne__restituisce not False, cioè True.

L'implementazione predefinita del __ne__metodo restituita "B.__ne__", non True.

Ora vediamo cosa succede quando si sovrascrive l'implementazione predefinita del A.__ne__metodo con l' not self == otherimplementazione e il A.__eq__metodo restituisce un valore diverso da NotImplemented:

class A:

    def __eq__(self, other):
        return True

    def __ne__(self, other):
        return not self == other


class B:

    def __ne__(self, other):
        return "B.__ne__"


assert (A() != B()) is False
  1. !=chiamate A.__ne__.
  2. A.__ne__chiamate ==.
  3. ==chiamate A.__eq__.
  4. A.__eq__ritorna True.
  5. A.__ne__restituisce not True, cioè False.

Anche in questo caso è __ne__stata restituita l'implementazione predefinita del metodo False.

Poiché questa implementazione non riesce a replicare il comportamento dell'implementazione predefinita del __ne__metodo quando il __eq__metodo restituisce NotImplemented, non è corretto.


All'ultimo esempio: "Poiché questa implementazione non riesce a replicare il comportamento dell'implementazione predefinita del __ne__metodo quando il __eq__metodo restituisce NotImplemented, non è corretto." - Adefinisce l'uguaglianza incondizionata. Così, A() == B(). Quindi A() != B() dovrebbe essere Falso , e lo è . Gli esempi forniti sono patologici (cioè __ne__non dovrebbero restituire una stringa e __eq__non dovrebbero dipendere da __ne__- piuttosto __ne__dovrebbero dipendere da __eq__, che è l'aspettativa predefinita in Python 3). Sono ancora -1 su questa risposta finché non puoi cambiare idea.
Aaron Hall

@AaronHall Dal riferimento al linguaggio Python : "Un ricco metodo di confronto può restituire il singleton NotImplementedse non implementa l'operazione per una data coppia di argomenti. Per convenzione, Falsee Truevengono restituiti per un confronto riuscito. Tuttavia, questi metodi possono restituire qualsiasi valore , quindi se l'operatore di confronto viene utilizzato in un contesto booleano (ad esempio, nella condizione di un'istruzione if), Python chiamerà bool()il valore per determinare se il risultato è vero o falso. "
Maggyero,

@AaronHall L'implementazione di __ne__uccisioni un'importante proprietà matematica, la simmetria del !=gestore. Questo operatore è binario, quindi il suo risultato dovrebbe dipendere dal tipo dinamico di entrambi gli operandi, non solo di uno. Questo è correttamente implementato nei linguaggi di programmazione tramite il doppio invio per il linguaggio che consente l'invio multiplo . In Python, che consente solo l'invio singolo, il doppio invio viene simulato restituendo il NotImplementedvalore.
Maggyero,

L'esempio finale ha due classi B,, che restituisce una stringa veritiera su tutti i controlli per __ne__e Ache restituisce Truesu tutti i controlli per __eq__. Questa è una contraddizione patologica. In una tale contraddizione, sarebbe meglio sollevare un'eccezione. Senza conoscenza B, Anon ha alcun obbligo di rispettare Bl'attuazione di __ne__ai fini della simmetria. A quel punto dell'esempio, il modo in cui gli Aattrezzi __ne__è irrilevante per me. Per favore, trova un caso pratico e non patologico per esprimere il tuo punto. Ho aggiornato la mia risposta per rivolgermi a te.
Aaron Hall

@AaronHall Per un esempio più realistico, vedere l'esempio SQLAlchemy fornito da @ShadowRanger. Si noti inoltre che il fatto che la propria implementazione dei __ne__lavori in casi d'uso tipici non lo rende corretto. Gli aerei Boeing 737 MAX hanno effettuato 500.000 voli prima degli incidenti ...
Maggyero il

-1

Se tutti __eq__, __ne__, __lt__, __ge__, __le__, e __gt__un senso per la classe, poi basta implementare __cmp__invece. Altrimenti, fai come stai facendo, a causa del bit che ha detto Daniel DiPaolo (mentre lo stavo testando invece di cercarlo;))


12
Il __cmp__()metodo speciale non è più supportato in Python 3.x, quindi dovresti abituarti a usare i ricchi operatori di confronto.
Don O'Donnell

8
O in alternativa, se sei in Python 2.7 o 3.x, anche il decoratore functools.total_ordering è abbastanza utile.
Adam Parkin

Grazie per il testa a testa. Tuttavia, ho realizzato molte cose in questo senso nell'ultimo anno e mezzo. ;)
Karl Knechtel
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.