__lt__ invece di __cmp__


100

Python 2.x ha due modi per sovraccaricare gli operatori di confronto, __cmp__o gli "operatori di confronto avanzati" come __lt__. Si dice che i ricchi sovraccarichi di confronto siano preferiti, ma perché è così?

Gli operatori di confronto avanzato sono più semplici da implementare ciascuno, ma è necessario implementarne diversi con una logica quasi identica. Tuttavia, se puoi usare l' cmpordinamento integrato e delle tuple, __cmp__diventa abbastanza semplice e soddisfa tutti i confronti:

class A(object):
  def __init__(self, name, age, other):
    self.name = name
    self.age = age
    self.other = other
  def __cmp__(self, other):
    assert isinstance(other, A) # assumption for this example
    return cmp((self.name, self.age, self.other),
               (other.name, other.age, other.other))

Questa semplicità sembra soddisfare le mie esigenze molto meglio che sovraccaricare tutti e 6 (!) I ricchi confronti. (Tuttavia, puoi ridurlo a "solo" 4 se ti affidi all '"argomento scambiato" / comportamento riflesso, ma a mio modesto parere ciò si traduce in un netto aumento della complicazione.)

Ci sono insidie ​​impreviste di cui devo essere consapevole se solo sovraccarico __cmp__?

Capisco il <, <=, ==, ecc operatori possono essere sovraccaricati per altri scopi, e può restituire qualsiasi oggetto a loro piace. Non sto chiedendo i meriti di tale approccio, ma solo le differenze quando si usano questi operatori per i confronti nello stesso senso in cui intendono per i numeri.

Aggiornamento: come ha sottolineato Christopher , cmpsta scomparendo in 3.x. Esistono alternative che rendono l'implementazione dei confronti facile come sopra __cmp__?


5
Vedi la mia risposta alla tua ultima domanda, ma in realtà c'è un design che renderebbe le cose ancora più facili per molte classi, inclusa la tua (in questo momento hai bisogno di un mixin, metaclass o class decorator per applicarlo): se è presente un metodo speciale chiave , deve restituire una tupla di valori e tutti i comparatori AND hash sono definiti in termini di quella tupla. A Guido è piaciuta la mia idea quando gliel'ho spiegato, ma poi mi sono dato da fare con altre cose e non sono mai riuscito a scrivere un PEP ... forse per 3.2 ;-). Nel frattempo continuo a usare il mio mixin per quello! -)
Alex Martelli

Risposte:


90

Sì, è facile implementare tutto in termini di, ad esempio, __lt__con una classe mixin (o una metaclasse o un decoratore di classi se i tuoi gusti vanno in questo modo).

Per esempio:

class ComparableMixin:
  def __eq__(self, other):
    return not self<other and not other<self
  def __ne__(self, other):
    return self<other or other<self
  def __gt__(self, other):
    return other<self
  def __ge__(self, other):
    return not self<other
  def __le__(self, other):
    return not other<self

Ora la tua classe può definire solo __lt__e moltiplicare ereditare da ComparableMixin (dopo qualsiasi altra base di cui ha bisogno, se ce ne sono). Un decoratore di classi sarebbe abbastanza simile, inserendo semplicemente funzioni simili come attributi della nuova classe che sta decorando (il risultato potrebbe essere microscopicamente più veloce in fase di esecuzione, a un costo altrettanto minuto in termini di memoria).

Ovviamente, se la tua classe ha un modo particolarmente veloce per implementare (ad esempio) __eq__e __ne__, dovrebbe definirli direttamente in modo che le versioni del mixin non siano usate (ad esempio, questo è il caso di dict) - in effetti __ne__potrebbe essere ben definito per facilitare che come:

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

ma nel codice sopra ho voluto mantenere la piacevole simmetria del solo utilizzo <;-). Per quanto riguarda il motivo per cui __cmp__doveva andare, dal momento che abbiamo fatto avere __lt__e amici, perché continuare a un'altra, modo diverso di fare esattamente la stessa cosa in giro? È così tanto peso morto in ogni runtime di Python (Classic, Jython, IronPython, PyPy, ...). Il codice che sicuramente non avrà bug è il codice che non c'è - da cui il principio di Python secondo cui idealmente dovrebbe esserci un modo ovvio per eseguire un'attività (C ha lo stesso principio nella sezione "Spirit of C" di lo standard ISO, btw).

Questo non significa che andiamo fuori del nostro modo di vietare le cose (per esempio, quasi equivalenza tra mixins e decoratori di classe per alcuni usi), ma sicuramente non significa che non mi piace portare in giro codice nei compilatori e / o tempi di esecuzione ridondanti solo per supportare più approcci equivalenti per eseguire esattamente la stessa attività.

Ulteriore modifica: in realtà c'è un modo ancora migliore per fornire il confronto E l'hashing per molte classi, incluso quello nella domanda: un __key__metodo, come ho menzionato nel mio commento alla domanda. Dato che non sono mai riuscito a scrivere il PEP per questo, devi attualmente implementarlo con un Mixin (& c) se ti piace:

class KeyedMixin:
  def __lt__(self, other):
    return self.__key__() < other.__key__()
  # and so on for other comparators, as above, plus:
  def __hash__(self):
    return hash(self.__key__())

È un caso molto comune che il confronto di un'istanza con altre istanze si riduca al confronto di una tupla per ciascuna con pochi campi e quindi l'hashing dovrebbe essere implementato esattamente sulla stessa base. Il __key__metodo speciale risolve direttamente la necessità.


Ci scusiamo per il ritardo @R. Pate, ho deciso che dal momento che dovevo modificare comunque avrei dovuto fornire la risposta più completa che potevo piuttosto che affrettarmi (e ho appena modificato di nuovo per suggerire la mia vecchia idea chiave che non ho mai avuto a che fare con PEPping, così come come per implementarlo con un mixin).
Alex Martelli

Mi piace molto l' idea chiave , la userò e vedrò come ci si sente. (Anche se denominato cmp_key o _cmp_key invece di un nome riservato.)

TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixinquando provo questo in Python 3. Vedi il codice completo su gist.github.com/2696496
Adam Parkin

2
In Python 2.7 + / 3.2 + puoi usare functools.total_orderinginvece di crearne uno tuo ComparableMixim. Come suggerito nella risposta di jmagnusson
Giorno

4
Usare <per implementare __eq__in Python 3 è una pessima idea, a causa di TypeError: unorderable types.
Antti Haapala

49

Per semplificare questo caso c'è un decoratore di classi in Python 2.7 + / 3.2 +, functools.total_ordering , che può essere utilizzato per implementare ciò che suggerisce Alex. Esempio dai documenti:

@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()))

9
total_orderingnon implementa __ne__però, quindi attenzione!
Flimm

3
@Flimm, non lo fa, ma __ne__. ma questo perché __ne__ha un'implementazione predefinita che delega a __eq__. Quindi non c'è niente a cui prestare attenzione qui.
Jan Hudec

deve definire almeno un'operazione di ordinamento: <> <=> = .... eq non è necessaria come ordine totale se! a <b e b <a quindi a = b
Xanlantos

9

Questo è coperto da PEP 207 - Rich Comparisons

Inoltre, __cmp__scompare in Python 3.0. (Notare che non è presente su http://docs.python.org/3.0/reference/datamodel.html ma È su http://docs.python.org/2.7/reference/datamodel.html )


Il PEP si occupa solo del motivo per cui sono necessari confronti ricchi, nel modo in cui gli utenti di NumPy vogliono che A <B restituisca una sequenza.

Non mi ero reso conto che se ne andasse definitivamente, questo mi rende triste. (Ma grazie per averlo fatto notare.)

Il PEP discute anche "perché" sono preferiti. Essenzialmente si riduce all'efficienza: 1. Non c'è bisogno di implementare operazioni che non hanno senso per il tuo oggetto (come le raccolte non ordinate). 2. Alcune raccolte hanno operazioni molto efficienti su alcuni tipi di confronti. Ricchi confronti consentono all'interprete di trarne vantaggio se li definisci.
Christopher,

1
Per quanto riguarda 1, se non hanno senso, non implementare cmp . In riferimento a 2, avere entrambe le opzioni può consentire di ottimizzare secondo necessità, pur continuando a prototipare e testare rapidamente. Nessuno dei due mi dice perché è stato rimosso. (Essenzialmente per me si riduce all'efficienza dello sviluppatore.) È possibile che i ricchi confronti siano meno efficienti con il fallback cmp in atto? Non avrebbe senso per me.

1
@R. Pate, come cerco di spiegare nella mia risposta, non c'è una vera perdita di generalità (poiché un mixin, decorator o metaclass, ti consente di definire facilmente tutto in termini di solo <se lo desideri) e quindi per tutte le implementazioni Python da portare in giro il codice ridondante che ricorre a cmp per sempre - solo per consentire agli utenti di Python di esprimere le cose in due modi equivalenti - funzionerebbe al 100% contro il grano di Python.
Alex Martelli

2

(Modificato il 17/6/17 per tenere conto dei commenti.)

Ho provato la risposta di mixin comparabile sopra. Ho avuto problemi con "Nessuno". Ecco una versione modificata che gestisce i confronti di uguaglianza con "Nessuno". (Non ho visto alcun motivo per preoccuparmi dei confronti della disuguaglianza con Nessuno come semantica priva):


class ComparableMixin(object):

    def __eq__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other and not other<self

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

    def __gt__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return other<self

    def __ge__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other

    def __le__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not other<self    

Come pensi che selfpotrebbe essere il Singleton Nonedi NoneTypee, allo stesso tempo implementare la tua ComparableMixin? E in effetti questa ricetta è un male per Python 3.
Antti Haapala

3
selfsarà mai essere None, in modo che possa andare ramo del tutto. Non usare type(other) == type(None); usa semplicemente other is None. Anziché speciale-involucro None, prova se l'altro tipo è un esempio del tipo di self, e restituire il NotImplementedsingleton se non: if not isinstance(other, type(self)): return NotImplemented. Fallo per tutti i metodi. Python potrà quindi dare all'altro operando la possibilità di fornire una risposta.
Martijn Pieters

1

Ispirato da Alex Martelli di ComparableMixin& KeyedMixinrisposte, mi si avvicinò con la seguente mixin. Ti consente di implementare un unico _compare_to()metodo, che utilizza confronti basati su chiavi simili a KeyedMixin, ma consente alla tua classe di scegliere la chiave di confronto più efficiente in base al tipo di other. (Nota che questo mixin non aiuta molto per gli oggetti che possono essere testati per l'uguaglianza ma non per l'ordine).

class ComparableMixin(object):
    """mixin which implements rich comparison operators in terms of a single _compare_to() helper"""

    def _compare_to(self, other):
        """return keys to compare self to other.

        if self and other are comparable, this function 
        should return ``(self key, other key)``.
        if they aren't, it should return ``None`` instead.
        """
        raise NotImplementedError("_compare_to() must be implemented by subclass")

    def __eq__(self, other):
        keys = self._compare_to(other)
        return keys[0] == keys[1] if keys else NotImplemented

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

    def __lt__(self, other):
        keys = self._compare_to(other)
        return keys[0] < keys[1] if keys else NotImplemented

    def __le__(self, other):
        keys = self._compare_to(other)
        return keys[0] <= keys[1] if keys else NotImplemented

    def __gt__(self, other):
        keys = self._compare_to(other)
        return keys[0] > keys[1] if keys else NotImplemented

    def __ge__(self, other):
        keys = self._compare_to(other)
        return keys[0] >= keys[1] if keys else NotImplemented
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.