Classes de domínio frequentemente precisam ser ordenadas: versões, prioridades, datas especiais, faixas, produtos ou registros. Implementar manualmente __lt__, __le__, __gt__ e __ge__ gera repetição e aumenta o risco de relações inconsistentes. O decorator functools.total_ordering completa os métodos de ordenação quando a classe fornece __eq__ e pelo menos uma comparação de ordem.
Neste guia, você aprenderá a criar ordens totais corretas, retornar NotImplemented, comparar chaves compostas, integrar com dataclasses, entender herança e desempenho, testar propriedades matemáticas e decidir quando escrever todos os métodos explicitamente.
O problema da repetição
class Versao:
def __init__(self, maior: int, menor: int):
self.maior = maior
self.menor = menor
Para permitir <, <=, > e >=, seria possível implementar quatro métodos semelhantes. Qualquer diferença entre eles pode produzir resultados contraditórios.
Primeiro uso de total_ordering
from functools import total_ordering
@total_ordering
class Versao:
def __init__(self, maior: int, menor: int):
self.maior = maior
self.menor = menor
def __eq__(self, outro: object) -> bool:
if not isinstance(outro, Versao):
return NotImplemented
return (self.maior, self.menor) == (outro.maior, outro.menor)
def __lt__(self, outro: object) -> bool:
if not isinstance(outro, Versao):
return NotImplemented
return (self.maior, self.menor) < (outro.maior, outro.menor)
Com igualdade e menor-que definidos, o decorator adiciona os demais operadores ausentes. A tupla concentra a chave de comparação e aproveita a ordem lexicográfica do Python.
O que o decorator gera
total_ordering procura um dos métodos __lt__, __le__, __gt__ ou __ge__. A partir dele e de __eq__, sintetiza os outros. Métodos já definidos na classe ou herdados não são substituídos.
Por que retornar NotImplemented
def __lt__(self, outro: object) -> bool:
if not isinstance(outro, Versao):
return NotImplemented
return self.chave < outro.chave
NotImplemented não é o mesmo que False. Ele informa ao interpretador que aquela combinação de tipos não é suportada e permite tentar a operação refletida do outro objeto. Se nenhum lado souber comparar, Python gera TypeError.
Não use raise NotImplemented
NotImplemented é um valor especial. NotImplementedError é uma exceção usada para métodos ainda não implementados. Em operadores de comparação, normalmente você deve retornar o valor, não lançar a exceção.
Centralizando a chave
@property
def chave(self) -> tuple[int, int]:
return self.maior, self.menor
Uma propriedade ou método privado reduz duplicação entre __eq__ e __lt__. A chave deve conter exatamente os campos que definem identidade de ordenação. Se igualdade usa campos diferentes da ordenação, as relações podem ficar incoerentes.
Ordem total e ordem parcial
Uma ordem total exige que elementos comparáveis tenham uma relação consistente: um é menor, igual ou maior. Alguns domínios não possuem ordem total natural. Conjuntos, dependências e permissões podem formar apenas uma ordem parcial. Forçar total_ordering nesses casos cria uma regra artificial e possivelmente enganosa.
Comparando tipos diferentes
v = Versao(3, 12)
# v < 10 deve falhar com TypeError, não fingir False
Retornar False para qualquer tipo incompatível faria parecer que os objetos são comparáveis. Retorne NotImplemented e deixe Python aplicar o protocolo correto.
Subclasses
isinstance(outro, Versao) aceita subclasses. Isso pode ser desejado quando todas compartilham a mesma semântica. Se uma subclasse adiciona campos que alteram identidade, comparar instâncias pode violar simetria. Em domínios estritos, use type(outro) is type(self).
Igualdade e hash
Ao definir __eq__, Python pode tornar a classe não hashable, pois igualdade personalizada exige um hash compatível. Se objetos forem imutáveis e usados como chaves, implemente __hash__ com os mesmos campos ou use dataclass frozen.
def __hash__(self) -> int:
return hash(self.chave)
Nunca calcule hash com campos mutáveis que podem mudar depois da inserção em um conjunto ou dicionário.
Integração com dataclass
from dataclasses import dataclass
@dataclass(order=True, frozen=True)
class Versao:
maior: int
menor: int
Quando a ordem segue a sequência de campos, @dataclass(order=True) costuma ser mais simples e gera igualdade e comparações. O guia de dataclasses no Python mostra opções como compare=False.
Quando total_ordering é melhor que dataclass
Use total_ordering quando a classe não é uma dataclass, quando a chave exige transformação, quando campos entram em ordens diferentes ou quando a comparação precisa validar compatibilidade.
Exemplo com prioridade
@total_ordering
class Tarefa:
def __init__(self, prioridade: int, criada_em: float, titulo: str):
self.prioridade = prioridade
self.criada_em = criada_em
self.titulo = titulo
@property
def chave(self):
return (-self.prioridade, self.criada_em, self.titulo)
def __eq__(self, outro):
if not isinstance(outro, Tarefa):
return NotImplemented
return self.chave == outro.chave
def __lt__(self, outro):
if not isinstance(outro, Tarefa):
return NotImplemented
return self.chave < outro.chave
Negar a prioridade coloca valores maiores primeiro quando a lista é ordenada de forma crescente. A data e o título funcionam como desempates determinísticos.
Valores especiais
Float com NaN não obedece às expectativas comuns de ordem total: comparações podem ser falsas inclusive consigo mesmo. Se o domínio admite NaN, normalize, rejeite ou defina uma política explícita.
None e sentinelas
Python 3 não ordena automaticamente None e números. Converta valores ausentes para uma chave comparável:
chave = (valor is None, valor if valor is not None else 0)
A posição de ausentes depende do primeiro componente. Documente se ficam no início ou no fim.
Strings e locale
Comparações de strings usam pontos de código, não regras linguísticas completas. Para ordenação humana, normalize caixa, acentos ou use uma chave de locale apropriada. A classe deve manter igualdade e ordem consistentes com essa normalização.
Desempenho
Os métodos gerados podem adicionar chamadas extras e stack traces mais complexos. Na maioria dos domínios, a diferença é irrelevante. Em loops extremamente quentes com milhões de comparações, implementar diretamente os seis métodos pode ser mais rápido e mais fácil de perfilar.
Herança e métodos existentes
O decorator não sobrescreve métodos já presentes ou herdados. Uma classe base pode introduzir uma comparação incompatível com o método definido na subclasse. Inspecione a hierarquia e prefira uma única fonte para a semântica.
Ordenação de coleções
versoes = [Versao(3, 12), Versao(3, 10), Versao(4, 0)]
print(sorted(versoes))
sorted() depende principalmente de __lt__. total_ordering é mais útil quando consumidores também usam <=, > e >=. Se só existe uma necessidade local de ordenação, uma função key= pode ser mais simples.
Preferindo key functions
sorted(produtos, key=lambda p: (p.preco, p.nome))
Não implemente ordem global em uma classe quando diferentes telas precisam ordenar por preço, nome, avaliação ou data. Métodos ricos devem representar uma ordem natural e estável do domínio.
Testando propriedades
Além de exemplos específicos, teste propriedades:
- Reflexividade da igualdade:
a == a. - Simetria: se
a == b, entãob == a. - Transitividade: se
a < beb < c, entãoa < c. - Coerência:
a <= bequivale aa < b or a == b. - Incompatíveis retornam NotImplemented e resultam em TypeError.
Testes baseados em propriedades ajudam a encontrar casos de desempate e valores extremos.
Erros comuns
- Retornar False para tipo incompatível: devolva NotImplemented.
- Usar campos diferentes em eq e lt: a ordem pode ficar incoerente.
- Forçar ordem total onde não existe: prefira relações explícitas.
- Ignorar NaN: floats especiais quebram propriedades.
- Implementar ordem global para todas as telas: use key functions.
- Esquecer hash: igualdade personalizada afeta uso em dict e set.
Exemplo completo: faixa de versão
from functools import total_ordering
@total_ordering
class Versao:
__slots__ = ("maior", "menor", "patch")
def __init__(self, maior: int, menor: int, patch: int = 0):
self.maior = maior
self.menor = menor
self.patch = patch
@property
def chave(self) -> tuple[int, int, int]:
return self.maior, self.menor, self.patch
def __eq__(self, outro: object) -> bool:
if type(outro) is not type(self):
return NotImplemented
return self.chave == outro.chave
def __lt__(self, outro: object) -> bool:
if type(outro) is not type(self):
return NotImplemented
return self.chave < outro.chave
def __hash__(self) -> int:
return hash(self.chave)
def __repr__(self) -> str:
return f"Versao{self.chave}"
A classe tem uma chave única para igualdade, ordem e hash. Tipos diferentes não são comparados silenciosamente.
Conclusão
functools.total_ordering reduz boilerplate e concentra a semântica de comparação em igualdade e um operador fundamental. Ele funciona melhor quando o domínio possui uma ordem total natural e a classe retorna NotImplemented para tipos incompatíveis.
A documentação oficial de total_ordering detalha o decorator. Prefira chaves consistentes, teste propriedades e implemente todos os operadores manualmente apenas quando desempenho ou clareza justificarem.







