__lt__ en lugar de __cmp__

100

Python 2.x tiene dos formas de sobrecargar los operadores de comparación, __cmp__o los "operadores de comparación enriquecidos" como __lt__. Se dice que se prefieren las abundantes sobrecargas de comparación, pero ¿por qué es así?

Los operadores de comparación ricos son más simples de implementar cada uno, pero debe implementar varios de ellos con una lógica casi idéntica. Sin embargo, si puede usar el orden integrado cmpy de tuplas, entonces se __cmp__vuelve bastante simple y cumple con todas las comparaciones:

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

Esta simplicidad parece satisfacer mis necesidades mucho mejor que sobrecargar las 6 (!) De las ricas comparaciones. (Sin embargo, puede reducirlo a "solo" 4 si confía en el "argumento intercambiado" / comportamiento reflejado, pero eso da como resultado un aumento neto de complicaciones, en mi humilde opinión).

¿Hay problemas imprevistos de los que deba ser consciente si solo sobrecargo __cmp__?

Entiendo la <, <=, ==, etc. operadores pueden ser sobrecargados para otros fines, y puede devolver cualquier objeto que les gusta. No estoy preguntando sobre los méritos de ese enfoque, sino solo sobre las diferencias al usar estos operadores para comparaciones en el mismo sentido en que se refieren a los números.

Actualización: como señaló Christopher , cmpestá desapareciendo en 3.x. ¿Existen alternativas que faciliten la implementación de comparaciones como las anteriores __cmp__?

Comunidad
fuente
5
Vea mi respuesta a su última pregunta, pero en realidad hay un diseño que facilitaría aún más las cosas para muchas clases, incluida la suya (ahora mismo necesita un decorador de mixin, metaclase o clase para aplicarlo): si hay un método especial clave , debe devolver una tupla de valores, y todos los comparadores Y hash se definen en términos de esa tupla. A Guido le gustó mi idea cuando se la expliqué, pero luego me ocupé de otras cosas y nunca llegué a escribir un PEP ... tal vez para 3.2 ;-). ¡Mientras tanto, sigo usando mi mezcla para eso! -)
Alex Martelli

Respuestas:

90

Sí, es fácil implementar todo en términos de, por ejemplo, __lt__con una clase mixin (o una metaclase, o un decorador de clases si su gusto es así).

Por ejemplo:

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

Ahora su clase puede definir __lt__heredar solo y multiplicar de ComparableMixin (después de cualquier otra base que necesite, si corresponde). Un decorador de clases sería bastante similar, simplemente insertando funciones similares como atributos de la nueva clase que está decorando (el resultado podría ser microscópicamente más rápido en tiempo de ejecución, con un costo igualmente mínimo en términos de memoria).

Por supuesto, si su clase tiene alguna forma particularmente rápida de implementar (por ejemplo) __eq__y __ne__, debe definirlas directamente para que las versiones de mixin no se utilicen (por ejemplo, ese es el caso dict); de hecho, __ne__podría definirse para facilitar Eso es todo:

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

pero en el código anterior quería mantener la agradable simetría de solo usar <;-). En cuanto a por qué __cmp__tenía que ir, ya que lo hizo tener __lt__y amigos, ¿por qué mantener a la otra, diferente manera de hacer exactamente lo mismo que en todo? Es un peso muerto en cada tiempo de ejecución de Python (Classic, Jython, IronPython, PyPy, ...). El código que definitivamente no tendrá errores es el código que no está allí, de ahí el principio de Python de que idealmente debería haber una forma obvia de realizar una tarea (C tiene el mismo principio en la sección "Espíritu de C" de el estándar ISO, por cierto).

Esto no quiere decir que vayamos fuera de nuestro camino para prohibir las cosas (por ejemplo, cerca de la equivalencia entre mixins y decoradores de clase para algunos usos), pero definitivamente hace media que no nos gusta llevar alrededor de código en los compiladores y / o tiempos de ejecución que existen de forma redundante solo para admitir múltiples enfoques equivalentes para realizar exactamente la misma tarea.

Edición adicional: en realidad, hay una manera aún mejor de proporcionar comparación Y hash para muchas clases, incluido eso en la pregunta: un __key__método, como mencioné en mi comentario a la pregunta. Como nunca llegué a escribir el PEP para él, actualmente debe implementarlo con un Mixin (& c) si le gusta:

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

Es un caso muy común que las comparaciones de una instancia con otras instancias se reduzcan a comparar una tupla para cada una con unos pocos campos, y luego, el hash debe implementarse exactamente sobre la misma base. El __key__método especial aborda esa necesidad directamente.

Alex Martelli
fuente
Perdón por la demora @R. Pate, decidí que, dado que tenía que editar de todos modos, debería proporcionar la respuesta más completa que pudiera en lugar de apresurarme (y acabo de editar nuevamente para sugerir mi vieja idea clave que nunca llegué a PEPping, así como cómo para implementarlo con un mixin).
Alex Martelli
Realmente me gusta esa idea clave , la usaré y veré cómo se siente. (Aunque se llama cmp_key o _cmp_key en lugar de un nombre reservado.)
TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixincuando pruebo esto en Python 3. Vea el código completo en gist.github.com/2696496
Adam Parkin
2
En Python 2.7 + / 3.2 + puede usar en functools.total_orderinglugar de construir el suyo propio ComparableMixim. Como se sugiere en la respuesta de jmagnusson
día
4
Usar <para implementar __eq__en Python 3 es una idea bastante mala, debido a TypeError: unorderable types.
Antti Haapala
49

Para simplificar este caso, hay un decorador de clases en Python 2.7 + / 3.2 +, functools.total_ordering , que se puede usar para implementar lo que sugiere Alex. Ejemplo de los documentos:

@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()))
jmagnusson
fuente
9
total_orderingaunque no se implementa __ne__, ¡así que ten cuidado!
Flimm
3
@Flimm, no es así, pero __ne__. pero eso es porque __ne__tiene una implementación predeterminada a la que delega __eq__. Así que no hay nada que vigilar aquí.
Jan Hudec
debe definir al menos una operación de pedido: <> <=> = .... eq no es necesario como pedido total si! a <b y b <a luego a = b
Xanlantos
9

Esto está cubierto por PEP 207 - Comparaciones enriquecidas

Además, __cmp__desaparece en Python 3.0. (Tenga en cuenta que no está presente en http://docs.python.org/3.0/reference/datamodel.html pero está en http://docs.python.org/2.7/reference/datamodel.html )

Christopher
fuente
El PEP solo se preocupa por por qué se necesitan comparaciones ricas, en la forma en que los usuarios de NumPy quieren que A <B devuelva una secuencia.
No me había dado cuenta de que definitivamente desaparecería, esto me entristece. (Pero gracias por señalar eso.)
El PEP también analiza "por qué" se prefieren. Básicamente, se reduce a la eficiencia: 1. No es necesario implementar operaciones que no tengan sentido para su objeto (como colecciones desordenadas). 2. Algunas colecciones tienen operaciones muy eficientes en algunos tipos de comparaciones. Las comparaciones enriquecidas permiten que el intérprete se aproveche de eso si las define.
Christopher
1
Re 1, si no tienen sentido, no implemente cmp . Re 2, tener ambas opciones puede permitirle optimizar según sea necesario, sin dejar de hacer prototipos y pruebas rápidamente. Ninguno me dice por qué se quitó. (Básicamente, para mí se reduce a la eficiencia del desarrollador). ¿Es posible que las comparaciones ricas sean menos eficientes con el respaldo de cmp implementado? Eso no tendría sentido para mí.
1
@R. Pate, como trato de explicar en mi respuesta, no hay una pérdida real en la generalidad (ya que un mixin, decorador o metaclase le permite definir todo fácilmente en términos de solo <si lo desea) y, por lo tanto, para todas las implementaciones de Python para llevar el código redundante que recurre a cmp para siempre, solo para permitir que los usuarios de Python expresen las cosas de dos maneras equivalentes, se ejecutaría al 100% en contra de la esencia de Python.
Alex Martelli
2

(Editado el 17/6/17 para tener en cuenta los comentarios).

Probé la respuesta de mezcla comparable anterior. Me metí en problemas con "Ninguno". Aquí hay una versión modificada que maneja comparaciones de igualdad con "Ninguno". (No vi ninguna razón para molestarme con las comparaciones de desigualdad con Ninguno por carecer de semántica):


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    
Gabriel Ferrer
fuente
¿Cómo crees que selfpodría ser el singleton Nonede NoneTypey al mismo tiempo implementar tu ComparableMixin? Y, de hecho, esta receta es mala para Python 3.
Antti Haapala
3
selfserá no ser None, de manera que la rama puede ir por completo. No lo use type(other) == type(None); simplemente use other is None. En lugar de especial-carcasa None, prueba si el otro tipo es una instancia de la clase de self, y devolver la NotImplementedsingleton si no: if not isinstance(other, type(self)): return NotImplemented. Haga esto para todos los métodos. Python entonces puede darle al otro operando la oportunidad de proporcionar una respuesta en su lugar.
Martijn Pieters
1

Inspirado por Alex Martelli de ComparableMixiny KeyedMixinrespuestas, se me ocurrió la siguiente mixin. Le permite implementar un solo _compare_to()método, que utiliza comparaciones basadas en claves similares a KeyedMixin, pero permite que su clase elija la clave de comparación más eficiente según el tipo de other. (Tenga en cuenta que este mixin no ayuda mucho para los objetos que se pueden probar para la igualdad pero no el orden).

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
Eli Collins
fuente