Las clases de dominio necesitan a menudo un orden natural: versiones, prioridades, rangos, productos, fechas o registros. Implementar manualmente __lt__, __le__, __gt__ y __ge__ crea repetición y aumenta el riesgo de resultados contradictorios. El decorator functools.total_ordering completa los métodos ausentes cuando una clase define __eq__ y al menos una comparación de orden.
Esta guía explica cómo diseñar un orden total correcto, devolver NotImplemented, comparar claves compuestas, integrar dataclasses, manejar herencia y valores especiales, medir rendimiento y probar propiedades matemáticas.
El problema de la repetición
class Version:
def __init__(self, mayor: int, menor: int):
self.mayor = mayor
self.menor = menor
Para soportar todos los operadores habría que escribir varios métodos casi iguales. Una pequeña diferencia puede hacer que a < b contradiga a >= b.
Primer uso de total_ordering
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, mayor: int, menor: int):
self.mayor = mayor
self.menor = menor
def __eq__(self, otro: object) -> bool:
if not isinstance(otro, Version):
return NotImplemented
return (self.mayor, self.menor) == (otro.mayor, otro.menor)
def __lt__(self, otro: object) -> bool:
if not isinstance(otro, Version):
return NotImplemented
return (self.mayor, self.menor) < (otro.mayor, otro.menor)
Con igualdad y menor-que definidos, el decorator añade los operadores restantes. La tupla centraliza la clave y aprovecha el orden lexicográfico de Python.
Qué genera el decorator
total_ordering busca uno de __lt__, __le__, __gt__ o __ge__. Junto con __eq__, ese método permite sintetizar los demás. Los métodos ya definidos o heredados no se reemplazan.
Por qué importa NotImplemented
def __lt__(self, otro: object) -> bool:
if not isinstance(otro, Version):
return NotImplemented
return self.clave < otro.clave
NotImplemented no significa False. Indica que esa combinación de operandos no está soportada y permite a Python probar la operación reflejada del otro objeto. Si ninguno sabe comparar, se genera TypeError.
No lances NotImplemented
NotImplemented es un valor especial. NotImplementedError es una excepción para métodos todavía no implementados. En comparaciones ricas normalmente se devuelve el valor.
Centralizar la clave
@property
def clave(self) -> tuple[int, int]:
return self.mayor, self.menor
Una propiedad o método privado evita duplicación entre __eq__ y __lt__. La clave debe contener exactamente los campos que definen identidad de orden. Si igualdad y orden utilizan campos distintos, la relación puede ser incoherente.
Orden total y orden parcial
Un orden total espera que los elementos comparables sean menores, iguales o mayores de forma consistente. Algunos dominios solo tienen orden parcial. Conjuntos, dependencias, capacidades y permisos pueden contener valores incomparables. Forzar un orden total crea una regla artificial.
Comparar tipos diferentes
version = Version(3, 12)
# version < 10 debe producir TypeError, no False silencioso
Devolver False para cualquier objeto incompatible sugiere falsamente que todos participan en el mismo orden. Devuelve NotImplemented.
Subclases
isinstance(otro, Version) acepta subclases. Es útil cuando comparten semántica. Si una subclase añade campos que cambian identidad, la comparación cruzada puede romper simetría. Los dominios estrictos pueden exigir type(otro) is type(self).
Igualdad y hash
Definir __eq__ puede hacer que la clase deje de ser hashable, porque la igualdad personalizada requiere un hash compatible. Los objetos inmutables usados como claves pueden implementar:
def __hash__(self) -> int:
return hash(self.clave)
No uses campos mutables que cambien después de insertar el objeto en un set o diccionario.
Integración con dataclass
from dataclasses import dataclass
@dataclass(order=True, frozen=True)
class Version:
mayor: int
menor: int
Cuando el orden sigue la declaración de campos, @dataclass(order=True) suele ser más simple y genera igualdad y comparaciones. La guía de dataclasses en Python explica compare=False y objetos frozen.
Cuándo total_ordering es mejor
Úsalo cuando la clase no es dataclass, la clave requiere normalización, los campos siguen otro orden o se necesita comprobar compatibilidad de operandos.
Ejemplo con prioridad
@total_ordering
class Tarea:
def __init__(self, prioridad: int, creada: float, titulo: str):
self.prioridad = prioridad
self.creada = creada
self.titulo = titulo
@property
def clave(self):
return (-self.prioridad, self.creada, self.titulo)
def __eq__(self, otro):
if not isinstance(otro, Tarea):
return NotImplemented
return self.clave == otro.clave
def __lt__(self, otro):
if not isinstance(otro, Tarea):
return NotImplemented
return self.clave < otro.clave
Negar la prioridad coloca valores mayores primero en un orden ascendente. La fecha y el título crean desempates deterministas.
Valores especiales
NaN no se comporta como un valor ordinario en un orden total. Algunas comparaciones son falsas incluso contra sí mismo. Si el dominio admite NaN, recházalo, normalízalo o define una política explícita.
None y sentinelas
Python 3 no ordena automáticamente None y números. Convierte ausencias en una clave comparable:
clave = (valor is None, valor if valor is not None else 0)
El primer componente controla si los ausentes aparecen al inicio o al final.
Strings y locale
Las strings se comparan por puntos de código Unicode, no por reglas lingüísticas completas. Para orden humano, normaliza mayúsculas, acentos o usa una clave de locale. Igualdad y orden deben compartir la misma normalización.
Rendimiento
Los métodos generados pueden añadir llamadas y stack traces más profundos. En la mayoría de aplicaciones la diferencia es insignificante. En loops con millones de comparaciones, métodos explícitos pueden ser más rápidos y fáciles de perfilar.
Herencia y métodos existentes
El decorator no reemplaza métodos presentes o heredados. Una clase base puede aportar una comparación incompatible con la subclase. Mantén la semántica en una capa clara.
Ordenar colecciones
versiones = [Version(3, 12), Version(3, 10), Version(4, 0)]
print(sorted(versiones))
sorted() depende principalmente de __lt__. total_ordering es más útil cuando los consumidores también usan <=, > y >=. Para una necesidad local, una función key= puede ser más simple.
Preferir key functions para varias vistas
sorted(productos, key=lambda producto: (producto.precio, producto.nombre))
No impongas un orden global si distintas pantallas ordenan por precio, nombre, valoración o fecha. La comparación rica debe representar un orden natural estable.
Probar propiedades
- Reflexividad de igualdad:
a == a. - Simetría: si
a == b, entoncesb == a. - Transitividad: si
a < byb < c, entoncesa < c. - Coherencia:
a <= bcoincide cona < b or a == b. - Operandos incompatibles terminan en TypeError.
Las pruebas basadas en propiedades encuentran casos extremos y errores de desempate.
Errores comunes
- Devolver False para tipos incompatibles: devuelve NotImplemented.
- Usar campos diferentes en eq y lt: orden e igualdad pueden contradecirse.
- Forzar orden total en un dominio parcial: expón relaciones explícitas.
- Ignorar NaN: rompe expectativas habituales.
- Definir un orden global para toda vista: usa key functions.
- Olvidar hash: igualdad personalizada afecta dict y set.
Ejemplo completo: versión semántica
from functools import total_ordering
@total_ordering
class Version:
__slots__ = ("mayor", "menor", "patch")
def __init__(self, mayor: int, menor: int, patch: int = 0):
self.mayor = mayor
self.menor = menor
self.patch = patch
@property
def clave(self) -> tuple[int, int, int]:
return self.mayor, self.menor, self.patch
def __eq__(self, otro: object) -> bool:
if type(otro) is not type(self):
return NotImplemented
return self.clave == otro.clave
def __lt__(self, otro: object) -> bool:
if type(otro) is not type(self):
return NotImplemented
return self.clave < otro.clave
def __hash__(self) -> int:
return hash(self.clave)
def __repr__(self) -> str:
return f"Version{self.clave}"
La clase usa una única clave para igualdad, orden y hash. Los tipos distintos no se comparan silenciosamente.
Conclusión
functools.total_ordering reduce boilerplate y concentra la semántica en igualdad y un operador fundamental. Funciona mejor cuando el dominio posee un orden total real y los operandos incompatibles devuelven NotImplemented.
La documentación oficial de total_ordering describe los métodos generados. Usa claves consistentes, prueba propiedades e implementa todos los operadores manualmente solo cuando rendimiento o depuración lo justifiquen.







