Trilha de aprendizado · Nível 11 · Tutorial 2

Definir igualdade e ordenação entre objetos

Ao concluir, você poderá estabelecer comparações coerentes para objetos de domínio e permitir sua ordenação quando essa operação tiver significado.

  • Nível: Avançado
  • Duração: 22 min
  • 9 passos
Definir igualdade e ordenação entre objetos

O que você vai percorrer

  1. Escolher o que torna dois objetos iguais Defina igualdade de valor para uma versão numérica, separando seus componentes essenciais de informações apenas descritivas. 2 min
  2. Implementar o contrato com __eq__ Implemente a igualdade de valor de Versao com os componentes significativos, tipos aceitos de forma explícita e o sinal correto para comparações fora do contrato. 3 min
  3. Entender NotImplemented e a resposta do outro operando Veja como Python negocia uma comparação de igualdade quando um dos objetos não reconhece o outro. 3 min
  4. Conhecer o protocolo das comparações de ordem Entenda quais métodos especiais atendem aos operadores de ordem e por que tipos incompatíveis podem resultar em TypeError. 2 min
  5. Construir uma ordem com critérios sucessivos Implemente uma ordem numérica coerente para versões, usando os componentes significativos na prioridade correta. 3 min
  6. Completar as relações com total_ordering Use um decorador da biblioteca padrão para derivar as relações de ordem que faltam, sem abrir mão de um contrato coerente. 2 min
  7. Escolher entre ordem do objeto e chave externa Use a ordem própria de Versao quando ela expressar o valor do domínio e use key quando o critério pertencer apenas à tarefa atual. 2 min
  8. Testar as propriedades do contrato Use propriedades e contraexemplos pequenos para revelar divergências entre igualdade, ordem e tipos incompatíveis. 3 min
  9. Aplicar e revisar as comparações de uma classe Conclua uma implementação de referência, execute verificações locais e explique como igualdade, ordem e tipos incompatíveis se comportam. 4 min

O que você vai aprender

  • Implementar __eq__ a partir dos atributos que definem a igualdade no domínio.
  • Retornar NotImplemented para comparações que a classe não sabe realizar.
  • Implementar uma relação de ordem e completar as demais com functools.total_ordering quando apropriado.
  • Verificar a coerência das comparações e o comportamento diante de tipos incompatíveis.

Antes de começar

  • Definir representações com __repr__ e __str__
  • Aplicar polimorfismo pelo comportamento dos objetos
  • Selecionar casos de teste e verificar exceções

Passo 1 de 9

Escolher o que torna dois objetos iguais

Defina igualdade de valor para uma versão numérica, separando seus componentes essenciais de informações apenas descritivas.

Igualdade depende do domínio

Mesmo objeto ou mesmo valor?

Duas variáveis podem apontar para o mesmo objeto; isso é identidade. Já a igualdade de valor responde outra pergunta: esses dois objetos representam o mesmo valor no domínio?

Essa resposta não é automática. Você escolhe quais atributos participam do contrato de igualdade da classe.

Identidade e igualdade de valor

Observe que referências diferentes podem representar o mesmo valor de versão.

Diagrama comparando duas referências para o mesmo objeto e duas instâncias distintas de versão com os mesmos componentes numéricos.

Identidade trata de referências; igualdade de valor trata dos dados que o domínio considera significativos.

O contrato de Versao

Quais campos definem uma versão?

No nosso caso, uma Versao tem três componentes numéricos: principal, secundaria e correcao. Ela também possui uma nota, como “versão estável” ou “implantação do cliente A”.

A nota ajuda a descrever ou apresentar a instância, mas não muda a versão numérica. Portanto, duas instâncias representam o mesmo valor quando os três componentes numéricos coincidem. Inicialmente, esse contrato vale somente entre instâncias do tipo concreto Versao.

Exemplo

Pares que o domínio considera iguais ou diferentes

Considere estas instâncias distintas:

v1 = Versao(2, 4, 1, nota="versão estável")
v2 = Versao(2, 4, 1, nota="implantação cliente A")
v3 = Versao(2, 4, 2, nota="correção liberada")
  • v1 e v2 devem ser iguais em valor: (2, 4, 1) é igual a (2, 4, 1).
  • v1 e v3 devem ser diferentes: o componente correcao mudou de 1 para 2.
  • v1 e v2 continuam sendo instâncias separadas, mesmo representando o mesmo valor.

Decida pelo contrato

Qual par deve ser igual?

Segundo o contrato definido para Versao, qual par deve representar o mesmo valor?

Passo 2 de 9

Implementar o contrato com __eq__

Implemente a igualdade de valor de Versao com os componentes significativos, tipos aceitos de forma explícita e o sinal correto para comparações fora do contrato.

Do operador ao método

O contrato em código

Quando Python avalia esquerda == direita, ele usa __eq__ para a comparação de igualdade. Para duas instâncias de Versao, o resultado suportado deve ser um booleano: True se principal, secundaria e correcao forem iguais; False se algum desses componentes diferir.

A nota continua fora da comparação: ela descreve a versão, mas não define seu valor no domínio.

Campos que participam da igualdade

A igualdade olha para a tripla numérica, não para a nota.

Diagrama comparando duas versões com os mesmos componentes principal, secundária e correção destacados em verde, mas notas descritivas diferentes em cinza; o resultado da igualdade é verdadeiro.

Instâncias diferentes podem ser iguais em valor quando os três componentes numéricos coincidem.

Mesmo tipo, campos explícitos

Implementação de __eq__

Compare somente os atributos que o contrato definiu como significativos.

python
class Versao:
    def __init__(self, principal, secundaria, correcao, nota):
        self.principal = principal
        self.secundaria = secundaria
        self.correcao = correcao
        self.nota = nota

    def __eq__(self, outro):
        if type(outro) is not type(self):
            return NotImplemented

        return (
            self.principal == outro.principal
            and self.secundaria == outro.secundaria
            and self.correcao == outro.correcao
        )

Por que usar type?

Aqui, type(outro) is not type(self) restringe o contrato ao mesmo tipo concreto antes de ler atributos de outro. Uma alternativa seria isinstance(outro, Versao), que também aceitaria subclasses. Não é a política escolhida neste contrato: uma subclasse pode acrescentar critérios de valor próprios.

Se o tipo não é aceito, retorne NotImplemented. Isso não quer dizer que as versões são diferentes; sinaliza que este método não realiza aquela comparação. No próximo passo, você verá como Python trata esse sinal ao avaliar o operador ==.

Resultados suportados e !=

Exemplo

Igualdade de valor em ação

v1 = Versao(2, 4, 1, "estável")
v2 = Versao(2, 4, 1, "revisada")
v3 = Versao(2, 4, 2, "correção nova")

print(v1 == v2)          # True: a nota não participa
print(v1 == v3)          # False: correcao é diferente
print(v1.__eq__("2.4.1"))  # NotImplemented: chamada direta
print(v1 != v2)          # False
print(v1 != v3)          # True

Você normalmente não precisa escrever __ne__. Quando __eq__ devolve True ou False, o comportamento herdado de != inverte esse resultado. Se __eq__ devolve NotImplemented, não há inversão direta por esse método.

Pratique o retorno correto

Complete __eq__

Complete o retorno para quando outro não tiver o mesmo tipo concreto:

def __eq__(self, outro):
    if type(outro) is not type(self):
        return _____

Preveja a comparação

Considere a = Versao(1, 0, 3, "interna") e b = Versao(1, 0, 3, "lançamento"). Qual é o resultado de a != b?

Passo 3 de 9

Entender NotImplemented e a resposta do outro operando

Veja como Python negocia uma comparação de igualdade quando um dos objetos não reconhece o outro.

Dois significados diferentes

Resultado falso ou operação desconhecida?

Em __eq__, False e NotImplemented comunicam coisas diferentes.

  • Retorne False quando a comparação é aceita, mas os valores são diferentes.
  • Retorne NotImplemented quando esse método não sabe comparar o outro operando.

Por exemplo, duas Versao podem ser comparadas entre si e resultar em False. Já comparar uma Versao com um objeto de outro tipo está fora do contrato definido pela classe.

Os caminhos de uma igualdade

A imagem contrasta uma comparação suportada com uma comparação que precisa ser delegada ao protocolo de Python.

Diagrama com dois caminhos: objetos do mesmo tipo levam a um resultado falso; objetos de tipos distintos levam a um sinal de operação não suportada e a uma tentativa no outro operando.

False encerra uma comparação suportada; NotImplemented pede que Python procure outra implementação.

Atenção

Não use NotImplemented como booleano

NotImplemented é um valor especial para retornar, não para lançar com raise. Também não escreva bool(NotImplemented) nem o use em um if: esse uso é inadequado e gera aviso de depreciação. Deixe o operador == conduzir o protocolo.

Chamada direta x operador

Um tipo que não compara com objetos externos

Execute este exemplo no seu interpretador Python.

python
class Versao:
    def __eq__(self, outro):
        if type(outro) is not Versao:
            return NotImplemented
        return True

versao = Versao()
print(versao.__eq__(42))  # NotImplemented
print(versao == 42)       # False
print(versao != 42)       # True

Quem decide o resultado final?

A chamada direta versao.__eq__(42) mostra exatamente o retorno daquele método: NotImplemented.

Já versao == 42 ativa o protocolo de comparação. Depois que Versao.__eq__ recusa o inteiro, Python pode consultar a implementação do outro operando. Se ninguém aceitar a igualdade, == recorre à identidade: objetos distintos resultam em False. Por consequência, != resulta em True.

A sequência de tentativas

Quando os tipos não têm herança entre si

Em uma expressão esquerdo == direito, Python tenta resolver a igualdade pelos métodos dos tipos envolvidos. Se a primeira implementação retorna NotImplemented, a outra pode responder.

Há uma regra importante: se o tipo do operando direito for uma subclasse estrita do tipo do esquerdo, Python dá prioridade à implementação do tipo mais específico, a do lado direito.

Observe quem responde

Cada método imprime quando é consultado.

python
class A:
    def __eq__(self, outro):
        print("A.__eq__")
        return NotImplemented

class B:
    def __eq__(self, outro):
        print("B.__eq__")
        return True

print(A() == B())
# Saída:
# A.__eq__
# B.__eq__
# True

class Base:
    def __eq__(self, outro):
        print("Base.__eq__")
        return NotImplemented

class Especial(Base):
    def __eq__(self, outro):
        print("Especial.__eq__")
        return True

print(Base() == Especial())
# Saída:
# Especial.__eq__
# True

Ordene a resolução

Para A() == B() no primeiro exemplo, ordene os eventos.

  1. B.__eq__ retorna True; a expressão resulta em True.
  2. Python chama B.__eq__(A()).
  3. A.__eq__ retorna NotImplemented.
  4. Python chama A.__eq__(B()).

Sem comparação suportada

Preveja o resultado

Se esquerdo.__eq__(direito) e a tentativa aplicável no outro operando retornam NotImplemented, então esquerdo == direito gera TypeError.

Resumo

Modelo mental

Use este roteiro ao ler ou implementar uma igualdade entre tipos diferentes.

  • False afirma que uma comparação suportada foi feita e os valores diferem.
  • NotImplemented informa que aquele método não sabe comparar o par recebido.
  • O operador == faz mais do que uma chamada direta: ele pode consultar o outro operando.
  • Uma subclasse estrita à direita tem prioridade por ser mais específica.
  • Se ninguém suporta a igualdade, Python decide por identidade; não lança TypeError nesse caso.

Passo 4 de 9

Conhecer o protocolo das comparações de ordem

Entenda quais métodos especiais atendem aos operadores de ordem e por que tipos incompatíveis podem resultar em TypeError.

Operadores e métodos especiais

A operação chamada por cada símbolo

Em uma expressão de ordem, Python consulta métodos especiais do tipo dos operandos. A correspondência direta é:

  • a < b → a.__lt__(b)
  • a <= b → a.__le__(b)
  • a > b → a.__gt__(b)
  • a >= b → a.__ge__(b)

Ao contrário de ==, definir __eq__ não dá significado automático a <, <=, > ou >=. Uma classe pode ter igualdade de valor e, ainda assim, não ter uma ordem definida.

Mapa das comparações de ordem

Os pares refletidos usam o operador de sentido oposto com os operandos trocados.

Diagrama mostrando a menor que associado a maior que com os operandos invertidos, e menor ou igual associado a maior ou igual com os operandos invertidos.

Se a tentativa de a < b não for aceita, Python pode consultar a operação correspondente b > a.

Quando a ordem não é suportada

NotImplemented não é um resultado de ordem

Uma implementação que não sabe ordenar o par deve retornar NotImplemented. Então Python tenta a operação refletida correspondente no outro operando — respeitando também a prioridade de subclasses vista anteriormente.

Para esquerda < direita, a tentativa refletida é direita.__gt__(esquerda). Para esquerda <= direita, é direita.__ge__(esquerda).

Se nenhuma implementação aceitar a operação, a expressão de ordem levanta TypeError. Isso difere da igualdade: quando ambos os lados não suportam ==, Python pode recorrer à identidade; em ordem, não há esse recurso final.

Uma classe com igualdade, mas sem ordem

Execute localmente para observar a falha de ordem.

python
class Versao:
    def __init__(self, principal, secundaria, correcao):
        self.principal = principal
        self.secundaria = secundaria
        self.correcao = correcao

    def __eq__(self, outro):
        if type(outro) is not Versao:
            return NotImplemented
        return (
            self.principal,
            self.secundaria,
            self.correcao,
        ) == (
            outro.principal,
            outro.secundaria,
            outro.correcao,
        )

v1 = Versao(1, 2, 0)
v2 = Versao(1, 2, 0)

print(v1 == v2)  # True
print(v1 < v2)   # TypeError: '<' not supported ...

Associe o protocolo

Operador direto e tentativa refletida

Relacione cada expressão à chamada que Python pode tentar caso a implementação direta retorne NotImplemented.

Toque em um item e depois no par correspondente.

Preveja o resultado

Tipos incompatíveis em uma ordem

Uma instância de Versao retorna NotImplemented em __lt__ ao receber um str, e o str também não suporta a comparação refletida. Qual é o resultado de versao < "2.0"?

Passo 5 de 9

Construir uma ordem com critérios sucessivos

Implemente uma ordem numérica coerente para versões, usando os componentes significativos na prioridade correta.

O primeiro desempate decide

Ordem lexicográfica

Uma versão é comparada em etapas: primeiro principal, depois secundaria e, somente se houver empate, correcao. O primeiro componente diferente decide qual versão vem antes.

Assim, 2.0.9 < 2.1.0 porque 0 < 1 na parte secundária. A correção nem precisa ser consultada.

Fluxo de desempate

A ordem de consulta é fixa: principal → secundária → correção.

Diagrama mostrando a comparação de duas versões: compara principal; se empatar, compara secundária; se empatar novamente, compara correção; o primeiro valor diferente decide.

Só avance para o próximo componente quando o anterior for igual.

Use uma tupla numérica

Uma expressão que preserva a prioridade

Tuplas são comparadas lexicograficamente em Python. Portanto, a tupla dos componentes traduz diretamente o contrato de ordem da versão.

Implementação de __lt__

Use a mesma política de tipos adotada em __eq__: somente instâncias do mesmo tipo concreto participam da comparação.

python
class Versao:
    def __init__(self, principal, secundaria, correcao, nota=""):
        self.principal = principal
        self.secundaria = secundaria
        self.correcao = correcao
        self.nota = nota

    def __eq__(self, outro):
        if type(outro) is not type(self):
            return NotImplemented
        return (
            self.principal,
            self.secundaria,
            self.correcao,
        ) == (
            outro.principal,
            outro.secundaria,
            outro.correcao,
        )

    def __lt__(self, outro):
        if type(outro) is not type(self):
            return NotImplemented
        return (
            self.principal,
            self.secundaria,
            self.correcao,
        ) < (
            outro.principal,
            outro.secundaria,
            outro.correcao,
        )

Exemplo

Por que não comparar o texto?

Versao(2, 10, 0) deve vir depois de Versao(2, 9, 0), pois 10 > 9.

Já os textos formatados '2.10.0' e '2.9.0' teriam ordem lexicográfica de caracteres, levando ao resultado incorreto: '2.10.0' < '2.9.0'. Compare os números, não a representação textual.

Dica

Igualdade e ordem usam o mesmo valor

Os três componentes numéricos participam tanto de __eq__ quanto de __lt__. A nota continua fora das duas comparações: ela descreve a versão, mas não define seu valor nem sua posição na ordem.

Pratique os desempates

Coloque em ordem crescente

Organize estas versões da menor para a maior, usando a ordem numérica dos componentes.

  1. 2.10.0
  2. 2.0.9
  3. 1.12.0
  4. 2.1.0

Confira o componente decisivo

Qual componente decide?

Na comparação Versao(3, 7, 99) < Versao(3, 12, 0), qual componente decide o resultado?

Passo 6 de 9

Completar as relações com total_ordering

Use um decorador da biblioteca padrão para derivar as relações de ordem que faltam, sem abrir mão de um contrato coerente.

Uma base pequena, relações completas

O decorador que completa a ordem

Quando uma classe já define uma igualdade coerente em __eq__ e uma relação de ordem, como __lt__, o decorador functools.total_ordering pode fornecer os demais métodos de ordem.

No caso de Versao, __eq__ define quando dois valores são iguais e __lt__ define qual vem antes. Essa é uma base suficiente para usar o decorador.

O que você escreve e o que é completado

Parta de __eq__ e __lt__; o decorador acrescenta as relações ausentes.

Diagrama mostrando __eq__ e __lt__ como métodos escritos na classe Versao, com setas para __le__, __gt__ e __ge__ como métodos completados por total_ordering.

Você implementa a igualdade e uma relação de ordem; total_ordering deriva as outras três relações de ordem.

Aplicando a Versao

Classe com ordem total

Coloque o decorador imediatamente acima da classe.

python
from functools import total_ordering

@total_ordering
class Versao:
    def __init__(self, principal, secundaria, correcao, nota):
        self.principal = principal
        self.secundaria = secundaria
        self.correcao = correcao
        self.nota = nota

    def __eq__(self, outro):
        if type(outro) is not Versao:
            return NotImplemented
        return (
            self.principal,
            self.secundaria,
            self.correcao,
        ) == (
            outro.principal,
            outro.secundaria,
            outro.correcao,
        )

    def __lt__(self, outro):
        if type(outro) is not Versao:
            return NotImplemented
        return (
            self.principal,
            self.secundaria,
            self.correcao,
        ) < (
            outro.principal,
            outro.secundaria,
            outro.correcao,
        )

Dica

Contrato continua sendo sua responsabilidade

total_ordering não cria __eq__, não substitui métodos que você já escreveu e não verifica se igualdade e ordem usam critérios compatíveis. Ele apenas deriva relações ausentes a partir do contrato que você forneceu.

Preveja os operadores derivados

Casos para observar

Execute após a definição da classe.

python
a = Versao(1, 4, 0, "estável")
b = Versao(1, 4, 0, "mesmo valor, outra nota")
c = Versao(1, 4, 1, "correção")

print(a <= b)  # True
print(c > a)   # True
print(a >= c)  # False
print(a <= "1.4.0")  # TypeError

Leitura dos resultados

a <= b é verdadeiro porque os valores são iguais. c > a é verdadeiro porque (1, 4, 1) vem depois de (1, 4, 0). Já a <= "1.4.0" termina em TypeError: os métodos recebem um tipo fora do contrato e retornam NotImplemented; nenhuma comparação de ordem suportada resta para a expressão.

Cheque a base do decorador

Complete a condição

Para usar @total_ordering, uma classe deve implementar __eq__ e pelo menos um método de ordem. No exemplo, esse método é ____.

Limite do total_ordering

Qual afirmação descreve corretamente total_ordering?

Passo 7 de 9

Escolher entre ordem do objeto e chave externa

Use a ordem própria de Versao quando ela expressar o valor do domínio e use key quando o critério pertencer apenas à tarefa atual.

Duas intenções de ordenação

Ordem do domínio ou da tela?

sorted(versoes) usa a ordem própria dos objetos: no caso de Versao, a comparação numérica por (principal, secundaria, correcao), definida por __lt__.

Já sorted(versoes, key=...) transforma cada objeto em uma chave para aquela ordenação. Isso é apropriado quando o critério é contextual — por exemplo, apresentar versões pela nota — e não deve mudar o significado de igualdade nem da ordem numérica de Versao.

Mesmo conjunto, resultados diferentes

A mesma lista pode ter uma ordem de domínio e uma ordem de apresentação.

Diagrama comparando três versões: à esquerda, versões em ordem numérica 1.2.0, 1.10.0 e 2.0.0; à direita, as mesmas versões reordenadas alfabeticamente pelas notas alfa, correção e estável.

A ordenação direta segue o valor da versão; a chave externa segue a necessidade da tela.

Ordenar diretamente e por key

Dois critérios, uma mesma lista

Considere que Versao já implementa __eq__ e __lt__ com total_ordering.

python
versoes = [
    Versao(2, 0, 0, "estável"),
    Versao(1, 10, 0, "correção"),
    Versao(1, 2, 0, "alfa"),
]

# Critério que pertence ao objeto: componentes numéricos.
por_versao = sorted(versoes)

# Critério da apresentação atual: texto da nota.
por_nota = sorted(versoes, key=lambda versao: versao.nota)

print([str(v) for v in por_versao])
# ['1.2.0', '1.10.0', '2.0.0']

print([v.nota for v in por_nota])
# ['alfa', 'correção', 'estável']

Exemplo

A igualdade permanece a mesma

Duas instâncias podem ter notas diferentes e, ainda assim, ser iguais se seus componentes numéricos forem iguais:

Versao(1, 0, 0, "inicial") == Versao(1, 0, 0, "lançamento") → True

Ordenar por key=lambda v: v.nota não altera esse resultado. A chave é usada somente para produzir a ordem pedida naquela chamada de sorted.

Empates e objetos sem ordem própria

Key não redefine o contrato

Uma chave pode empatar mesmo para objetos diferentes. Por exemplo, duas versões distintas podem ter a nota "beta"; elas continuam diferentes pela igualdade numérica, mas recebem a mesma chave textual.

Além disso, uma classe que não possui uma ordem de domínio ainda pode ser ordenada por key, desde que os valores retornados pela chave sejam comparáveis.

Ordenar objetos sem __lt__

A classe abaixo não define comparações. Ainda assim, a lista pode ser ordenada pela prioridade numérica extraída por key.

python
class Tarefa:
    def __init__(self, titulo, prioridade):
        self.titulo = titulo
        self.prioridade = prioridade

    def __repr__(self):
        return f"Tarefa({self.titulo!r}, {self.prioridade})"

pendentes = [
    Tarefa("documentar", 2),
    Tarefa("corrigir", 1),
]

print(sorted(pendentes, key=lambda tarefa: tarefa.prioridade))
# [Tarefa('corrigir', 1), Tarefa('documentar', 2)]

Decida o critério

Cenário

Uma tela de lançamentos deve mostrar objetos Versao em ordem alfabética de nota, mas a classe já possui ordem numérica coerente. Qual é a melhor escolha?

Passo 8 de 9

Testar as propriedades do contrato

Use propriedades e contraexemplos pequenos para revelar divergências entre igualdade, ordem e tipos incompatíveis.

Propriedades que os testes devem proteger

Um contrato, não casos isolados

Além de testar exemplos pontuais, teste propriedades do contrato.

Para igualdade: uma versão é igual a si mesma (reflexividade); se a == b, então b == a (simetria); e, se a == b e b == c, então a == c (transitividade).

Para a ordem estrita <: nunca vale a < a (irreflexividade); se a < b, não pode valer b < a (assimetria); e, se a < b e b < c, então a < c (transitividade). Em uma ordem total, objetos iguais não precedem um ao outro, e dois valores diferentes comparáveis precisam ter uma direção: a < b ou b < a.

O que cada propriedade observa

Use esta sequência como mapa para escolher exemplos de teste.

Diagrama com três objetos de versão em ordem crescente e dois objetos distintos com os mesmos componentes numéricos, destacando igualdade e relações de menor que.

Compare instâncias de mesmo valor, pares com desempate e trios crescentes: cada grupo revela uma propriedade diferente.

Dica

Escolha casos que isolam uma regra

Use notas diferentes para confirmar que a nota não participa da igualdade. Para a ordem, crie pares em que o desempate ocorre primeiro em principal, depois em secundaria e, por fim, em correcao.

Uma bateria enxuta com pytest

Testes de propriedades e desempates

Este trecho pressupõe que Versao já está definida em versao.py com __eq__, __lt__ e total_ordering.

python
import pytest

from versao import Versao


def test_igualdade_eh_reflexiva_simetrica_e_transitiva():
    a = Versao(1, 2, 3, "estável")
    b = Versao(1, 2, 3, "publicada")
    c = Versao(1, 2, 3, "revisada")

    assert a == a
    assert a == b and b == a
    assert a == b and b == c and a == c


def test_iguais_nao_precedem_um_ao_outro():
    a = Versao(2, 0, 1, "nota A")
    b = Versao(2, 0, 1, "nota B")

    assert a == b
    assert not (a < b)
    assert not (b < a)


@pytest.mark.parametrize(
    ("menor", "maior"),
    [
        (Versao(1, 9, 9, ""), Versao(2, 0, 0, "")),
        (Versao(2, 1, 9, ""), Versao(2, 2, 0, "")),
        (Versao(2, 2, 1, ""), Versao(2, 2, 2, "")),
    ],
)
def test_cada_componente_pode_decidir_o_desempate(menor, maior):
    assert menor < maior
    assert not (maior < menor)


def test_menor_que_eh_irreflexivo_e_transitivo():
    a = Versao(1, 0, 0, "")
    b = Versao(1, 1, 0, "")
    c = Versao(2, 0, 0, "")

    assert not (a < a)
    assert a < b and b < c and a < c

Incompatibilidade: método direto e operador

Teste os dois níveis

Um método da classe deve devolver NotImplemented ao receber um tipo fora do contrato. Já o operador executa o protocolo completo: para igualdade, o resultado final entre objetos sem suporte costuma ser False; para ordem, a ausência de suporte dos dois lados produz TypeError.

Faça as expressões incompatíveis nas duas posições. Isso evita testar apenas o caminho em que Versao ficou à esquerda.

Tipos incompatíveis nos dois sentidos

Adicione estes testes à sua suíte.

python
import pytest

from versao import Versao


def test_metodos_recusam_tipo_fora_do_contrato():
    versao = Versao(1, 0, 0, "inicial")

    assert versao.__eq__("1.0.0") is NotImplemented
    assert versao.__lt__("1.0.0") is NotImplemented


def test_igualdade_incompativel_termina_em_false_nos_dois_sentidos():
    versao = Versao(1, 0, 0, "inicial")

    assert (versao == "1.0.0") is False
    assert ("1.0.0" == versao) is False


def test_ordem_incompativel_falha_nos_dois_sentidos():
    versao = Versao(1, 0, 0, "inicial")

    with pytest.raises(TypeError):
        versao < "1.0.0"

    with pytest.raises(TypeError):
        "1.0.0" < versao

Não confunda igualdade com ordem

Exemplo

Contraexemplo mínimo

Imagine uma implementação defeituosa: __eq__ compara apenas (principal, secundaria), mas __lt__ compara (principal, secundaria, correcao).

Então Versao(1, 0, 0, "") == Versao(1, 0, 1, "") seria verdadeiro, mas a primeira também seria menor que a segunda. Isso viola o contrato: valores iguais não podem preceder um ao outro.

Um par pequeno como esse localiza rapidamente quais campos estão divergindo entre __eq__ e __lt__.

Diagnostique o contrato

Você suspeita que __eq__ esqueceu o campo correcao, enquanto __lt__ continua comparando os três componentes. Quais dois testes você escreveria primeiro para expor esse erro? Inclua também como verificaria o comportamento com uma string incompatível.

Escreva pelo menos 120 caracteres (0/120).

Passo 9 de 9

Aplicar e revisar as comparações de uma classe

Conclua uma implementação de referência, execute verificações locais e explique como igualdade, ordem e tipos incompatíveis se comportam.

Contrato reunido em uma classe

O que a classe deve preservar

Nesta prática, os campos principal, secundaria e correcao definem o valor de uma Versao. A nota serve apenas para apresentação: ela não participa de == nem da ordem direta.

O contrato aceita comparações somente entre objetos do mesmo tipo concreto. Fora dele, os métodos retornam NotImplemented. Com total_ordering, __eq__ e __lt__ bastam para disponibilizar as demais relações de ordem.

Dois critérios, dois resultados

Diagrama comparando versões por uma sequência de três componentes numéricos e, em separado, ordenando as mesmas versões pelas notas descritivas.

A ordem própria consulta os componentes numéricos; uma chave externa pode consultar a nota sem alterar o contrato da classe.

Crie o arquivo versao.py

No seu computador, crie uma pasta vazia e salve este conteúdo como versao.py. Leia especialmente os dois retornos de NotImplemented: eles delimitam o que a classe sabe comparar.

python
from functools import total_ordering


@total_ordering
class Versao:
    def __init__(self, principal, secundaria, correcao, nota):
        self.principal = principal
        self.secundaria = secundaria
        self.correcao = correcao
        self.nota = nota

    def __repr__(self):
        return (
            f"Versao({self.principal}, {self.secundaria}, "
            f"{self.correcao}, {self.nota!r})"
        )

    def __eq__(self, outro):
        if type(self) is not type(outro):
            return NotImplemented
        return (
            self.principal,
            self.secundaria,
            self.correcao,
        ) == (
            outro.principal,
            outro.secundaria,
            outro.correcao,
        )

    def __lt__(self, outro):
        if type(self) is not type(outro):
            return NotImplemented
        return (
            self.principal,
            self.secundaria,
            self.correcao,
        ) < (
            outro.principal,
            outro.secundaria,
            outro.correcao,
        )

Execute verificações locais

Crie o teste completo

Salve este conteúdo como test_versao.py, na mesma pasta de versao.py. Os testes verificam igualdade de valor, desempate lexicográfico, operadores completados pelo decorador, ordenação por chave e incompatibilidade de tipos.

python
import pytest

from versao import Versao


def test_igualdade_ignora_nota():
    primeira = Versao(1, 2, 3, "publicada")
    segunda = Versao(1, 2, 3, "revisada")

    assert primeira == segunda
    assert not primeira != segunda


def test_ordem_usa_componentes_em_sequencia():
    assert Versao(1, 2, 9, "a") < Versao(1, 10, 0, "b")
    assert Versao(1, 10, 0, "a") < Versao(2, 0, 0, "b")


def test_total_ordering_completa_operadores():
    menor = Versao(1, 0, 0, "inicial")
    igual = Versao(1, 0, 0, "mesmo valor")
    maior = Versao(1, 0, 1, "correcao")

    assert menor <= igual
    assert maior > menor
    assert maior >= igual
    assert not menor > igual


def test_sorted_direto_e_por_chave_externa():
    versoes = [
        Versao(2, 0, 0, "beta"),
        Versao(1, 10, 0, "zeta"),
        Versao(1, 2, 9, "alpha"),
    ]

    assert [v.nota for v in sorted(versoes)] == ["alpha", "zeta", "beta"]
    assert [v.nota for v in sorted(versoes, key=lambda v: v.nota)] == [
        "alpha", "beta", "zeta"
    ]


def test_tipos_incompativeis():
    versao = Versao(1, 2, 3, "estavel")

    assert Versao.__eq__(versao, "1.2.3") is NotImplemented
    assert Versao.__lt__(versao, "1.2.3") is NotImplemented
    assert versao == "1.2.3" is False
    assert versao != "1.2.3" is True

    with pytest.raises(TypeError):
        versao < "1.2.3"

Exemplo

Comando e resultado esperado

No terminal aberto nessa pasta, execute:

python -m pytest -q

Resultado esperado:

5 passed

Se pytest não estiver disponível no seu ambiente, instale-o no ambiente virtual do projeto com python -m pip install pytest e rode o comando novamente.

Revise o comportamento observado

Registre suas evidências

Após executar o teste, relate o que você observou. Inclua: o efeito de notas diferentes na igualdade, um desempate da ordem lexicográfica, um operador fornecido por total_ordering e a diferença entre == e < ao comparar uma Versao com uma string.

Escreva pelo menos 180 caracteres (0/180).

Síntese e próximo passo

Resumo

Contrato concluído

Você reuniu as decisões de comparação em uma implementação verificável.

  • Igualdade de valor comparou apenas os três componentes numéricos; a nota ficou fora desse contrato.
  • eq e lt recusaram tipos fora do contrato com NotImplemented.
  • A tupla de componentes expressou a prioridade principal, secundária e de correção na ordem lexicográfica.
  • total_ordering completou as relações de ordem que não foram escritas explicitamente.
  • sorted(versoes) usou a ordem do objeto; sorted(versoes, key=lambda v: v.nota) aplicou uma necessidade contextual.
  • Na igualdade incompatível, o operador terminou em False; na ordem incompatível, terminou em TypeError.

Encerramento

Agora você pode decidir quando objetos de domínio representam o mesmo valor e quando possuem uma ordem natural. No próximo tutorial, a discussão avança para a coerência entre igualdade e hash.

Tutorial concluído

Parabéns! Você concluiu: Definir igualdade e ordenação entre objetos

Você concluiu a implementação e a revisão de comparações coerentes para objetos.

Baixe o Aplicativo agora para ter acesso a + de 5000 cursos gratuitos, exercícios, certificado e muito conteúdo sem pagar nada!

  • Cursos online 100% gratuitos do início ao fim

    Milhares de cursos online em vídeo, ebooks e áudiobooks.

  • Mais de 60 mil exercícios gratuitos

    Para testar seus conhecimentos no decorrer dos cursos online

  • Certificado Digital gratuito válido em todo o Brasil

    Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail

Aplicativo Cursa na tela de ebook, na tela de curso em vídeo e na tela de exercícios do curso, mais o certificado de conclusão de curso