python - getitem - __lt__ en lugar de__cmp__
python metaclass (5)
(Editado el 17/06/17 para tener en cuenta los comentarios).
Probé la respuesta mixin comparable anterior. Tuve problemas con "None". Aquí hay una versión modificada que maneja las comparaciones de igualdad con "Ninguno". (No vi ninguna razón para preocuparse por las comparaciones de desigualdad con None como carente 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
Python 2.x tiene dos formas de sobrecargar operadores de comparación, __cmp__
o los "operadores de comparación ricos" como __lt__
. Se dice que las abundantes sobrecargas de comparación son preferidas, 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 los pedidos __cmp__
cmp
y tuple, entonces __cmp__
vuelve bastante simple y cumple 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 comparaciones ricas. (Sin embargo, puede bajarlo a "solo" 4 si confía en el "argumento intercambiado" / comportamiento reflejado, pero eso resulta en un aumento neto de complicaciones, en mi humilde opinión).
¿Hay algún error imprevisto del que deba enterarse si solo sobrecargamos __cmp__
?
Entiendo que los operadores <
, <=
, ==
, etc. pueden sobrecargarse para otros fines y pueden devolver cualquier objeto que quieran. No estoy preguntando sobre los méritos de ese enfoque, sino solo sobre las diferencias al usar estos operadores para las comparaciones en el mismo sentido que significan para los números.
Actualización: como señaló Christopher, cmp
está desapareciendo en 3.x. ¿Hay alguna alternativa que haga que la implementación de comparaciones sea tan fácil como el anterior __cmp__
?
Esto está cubierto por PEP 207 - Comparaciones ricas
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 )
Inspirado por las respuestas ComparableMixin
y KeyedMixin
Alex Martelli, se me ocurrió la siguiente mezcla. Le permite implementar un único método _compare_to()
, que utiliza comparaciones basadas en claves similares a KeyedMixin
, pero le permite a su clase elegir la clave de comparación más eficiente en función del tipo de other
. (Tenga en cuenta que este mixin no ayuda mucho para los objetos que pueden ser probados para la igualdad pero no para 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
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()))
Sí, es fácil implementar todo en términos de, por ejemplo, __lt__
con una clase mixin (o una metaclass, o un decorador de clase si tu gusto se ejecuta de esa manera).
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 solo __lt__
y heredar 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, a un costo igualmente pequeño en términos de memoria).
Por supuesto, si su clase tiene alguna forma particularmente rápida de implementar (por ejemplo) __eq__
y __ne__
, debe definirlos directamente para que las versiones de la mezcla no sean de uso (por ejemplo, ese es el caso para dict
) - de hecho __ne__
bien podría ser definido para facilitar eso como:
def __ne__(self, other):
return not self == other
pero en el código anterior quería mantener la simetría agradable de solo usar <
;-). En cuanto a por qué __cmp__
tuvo que ir, ya que tuvimos __lt__
y amigos, ¿por qué mantener otra, diferente manera de hacer exactamente lo mismo? Es un peso muerto en cada runtime de Python (Classic, Jython, IronPython, PyPy, ...). El código que definitivamente no tendrá errores es el código que no está allí, por lo que el principio de Python de que idealmente debería existir 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 significa que salgamos de nuestro camino para prohibir cosas (por ejemplo, casi equivalencia entre mixins y decoradores de clase para algunos usos), pero definitivamente significa que no nos gusta llevar el código en los compiladores y / o tiempos de ejecución que redundantemente existe solo para soportar múltiples enfoques equivalentes para realizar exactamente la misma tarea.
Edición adicional: en realidad hay una forma aún mejor de proporcionar comparación Y hashing para muchas clases, incluida la pregunta: un método __key__
, como mencioné en mi comentario a la pregunta. Como nunca llegué a escribir el PEP, debe implementarlo con Mixin (& c) si lo desea:
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 uno con unos pocos campos, y luego, el hashing debería implementarse exactamente sobre la misma base. El método especial __key__
aborda esa necesidad directamente.