Como comparar objetos no Python?
Nesse artigo vou mostrar como funciona o operador de igualdade no Python e como alteramos seu comportamento, controlando mais nosso código.
Tenho um sistema em Python que armazena os filmes que eu tenho em uma lista, para organização, com uma função que pega todos os filmes e retorna uma lista com eles:
class Filme():
def __init__(self, titulo, diretor):
self.titulo = titulo
self.diretor = diretor
def __str__(self):
return self.titulo + ‘ - ‘ + self.diretor
def pega_todos_os_filmes():
## implementação da função
meus_filmes = pega_todos_os_filmes()
for filme in meus_filmes:
print(filme)
Vamos ver como ficou:
A Teoria de Tudo - James Marsh
La La Land - Damien Chazelle
O Poderoso Chefão - Francis Ford Coppola
Ok, temos nossa lista de filmes!
Depois de um tempo, queria descobrir se eu tinha um filme. Como podemos fazer isso?
Comparando objetos com o operador ==
Para descobrir se eu já tenho um filme, criei uma função que verifica se o filme que eu quero saber está na nossa lista de filmes:
def tenho_o_filme(filme_procurado):
meus_filmes = pega_todos_os_filmes()
for filme in meus_filmes:
if filme_procurado == filme:
return True
return False
filme_procurado = Filme(‘A Teoria de Tudo’, ‘James Marsh’)
if tenho_o_filme(filme_procurado):
print(‘Tenho o filme!’)
else:
print(‘Não tenho :(‘)
Rodamos nosso código e esse foi o resultado:
Não tenho :(
Ué! A gente já viu que o filme A Teoria de Tudo está na nossa lista, então o que está dando errado?
Sabemos que o operador ==
funciona bem para números e strings:
x = 700
y = 700
print(x == y)
a = ‘Yan’
b = ‘Yan’
print(a == b)
O resultado:
True
True
Mas então por que ele não funciona como queremos com nossa classe Filme
?
Como o Python compara objetos?
A questão é que o Python não sabe como deve comparar os objetos de tipo Filme
quando utilizamos o operador ==
. Que valor deve ser comparado? O titulo? O diretor? E de que forma? O Python não sabe!
Com essa dúvida, o Python opta por não verificar valor nenhum, mas sim a identidade dos objetos. Isto é, ele checa se as variáveis em comparação apontam para o mesmo objeto na memória. Podemos verificar a identidade de um objeto através da função id():
a = Filme(‘A Teoria de Tudo’, ‘James Marsh’)
b = Filme(‘A Teoria de Tudo’, ‘James Marsh’)
print(id(a))
print(id(b))
O resultado será parecido com esse:
139789394500856
139789394500800
Repare que, de fato, os dois valores de identidade são diferentes! Se indicássemos que a variável b
aponta para o objeto da variável a
, o resultado seria diferente:
a = Filme(‘A Teoria de Tudo’, ‘James Marsh’)
b = a
print(a == b)
Dessa vez:
True
Isso ocorre porque, nesse caso, as duas variáveis apontam para o mesmo objeto, ou seja, possuem a mesma identidade:
print(id(a))
print(id(b))
O resultado:
139824816890912
139824816890912
Vamos ver como isso funciona com os números inteiros e as strings que testamos lá atrás:
x = 700
y = 700
print(‘Os inteiros:’)
print(id(x))
print(id(y))
a = ‘Yan’
b = ‘Yan’
print(‘As strings:’)
print(id(a))
print(id(b))
E o resultado:
Os inteiros:
139789394500800
139789394500856
As strings:
139789394500970
139789394500970
Bem, os números têm identidades diferentes, enquanto as strings apresentaram identidades iguais. Isso significa que a comparação ==
com strings compara o id
, e com números inteiros não? Não exatamente…
Como o Python compara tipos primitivos?
De fato, os ids eram os mesmos com as strings, mas, mesmo assim, não era isso que estava sendo comparado. Os tipos primitivos no Python já têm implementado nativamente suas próprias maneiras de se comparar. No caso do int e da string, o que se compara são seus valores.
Como podemos mudar esse comportamento padrão do ==
, então? Afinal, comparar o id dos objetos não é o que queremos! Precisamos, de alguma forma, basear nossa comparação em algum atributo do Filme
.
Quem vem de outras linguagens de programação, como Java, já deve conhecer métodos como o equals(), que pode ser sobrescrito para ter o comportamento alterado. Como isso funciona no Python?
Conhecendo a rich comparison
No Python temos como implementar algo similar ao equals()
, mas ainda mais poderoso - a comparação rica, ou, como é tecnicamente conhecida, rich comparison . Com ela, podemos definir os seguintes métodos de comparação em uma classe:
__eq__()
, chamado pelo operador==
__ne__()
, chamado pelo operador!=
__gt__()
, chamado pelo operador>
__lt__()
, chamado pelo operador<
__ge__()
, chamado pelo operador>=
__le__()
, chamado pelo operador<=
No nosso caso, como estamos tratando de comparações de igualdade, focaremos no método __eq__()
(mas é importante notar a possibilidade de implementação de todos os tipos básicos de comparação!).
Precisamos, primeiro, saber o que queremos que seja comparado. Como precisamos que a comparação foque em algo único de cada filme, usaremos o próprio título. Então vamos implementar:
class Filme():
## código omitido
def __eq__(self, outro_filme):
return self.titulo == outro_filme.titulo
Agora que aplicamos isso ao nosso código, vamos tentar procurar um filme em nosso acervo novamente:
def tenho_o_filme(filme_procurado):
meus_filmes = pega_todos_os_filmes()
for filme in meus_filmes:
if filme_procurado == filme:
return True
return False
filme_procurado = Filme(‘A Teoria de Tudo’, ‘James Marsh’)
if tenho_o_filme(filme_procurado):
print(‘Tenho o filme!’)
else:
print(‘Não tenho :(‘)
Dessa vez:
Tenho o filme!
Certo!
Simplificando nossa verificação com o operador in
Podemos, ainda, simplificar o código em nossa função tenho_o_filme()
utilizando o operador in para verificar se o filme já está na lista, já que este operador também se baseia no retorno de ==
:
def tenho_o_filme(filme_procurado):
meus_filmes = pega_todos_os_filmes()
return filme_procurado in meus_filmes
Peculiaridades da rich comparison
Há alguns aspectos interessantes de nosso código que valem a pena destacar. Em primeiro lugar, repare que não implementamos o método ne(), relativo ao operador !=. Mas o que acontece, então, se tentarmos usar esse operador?
A partir do Python 3, o operador !=
retorna automaticamente o inverso do retorno do método __eq__()
, se o método __ne__()
não tiver sido implementado.
Apesar disso, se sua classe herdar de outra em que o método __ne__()
é definido (como é nos tipos primitivos), o comportamento do !=
se baseará no que é especificado nesse método, não no inverso do __eq__()
!
Por conta disso, é recomendável sempre declarar o método __ne__()
quando quisermos implementar o __eq__()
, tanto pela compatibilidade com o Python 2 (que sem esse método vai comparar os ids), quanto para evitar possíveis problemas em nosso código.
Outro aspecto interessante de nosso código é que optamos por retornar um valor boolean. Sim, optamos!
A rich comparison te permite um retorno de qualquer tipo. Isso significa que uma comparação do tipo a > b
não precisa, necessariamente, nos devolver um boolean, mas pode devolver qualquer coisa que o desenvolvedor implementar.
Em geral, optamos por retornar um boolean, mas há grandes e importantes projetos que alteram esse comportamento, como o SQLAlchemy, que usa essa feature para facilitar a criação de queries SQL, e até a biblioteca numpy, inspiração para a criação da rich comparison.
Conclusão
Nesse post, aprendemos a implementar métodos de comparação para nossas classes no Python utilizando a técnica de comparação rica. Agora entendemos como funcionam os operadores ==
, !=
, >
, <
, >=
, <=
.
E aí? Agora não teremos problemas com comparações entre objetos que nós mesmos implementamos no Python! Se quiser se aprofundar mais na linguagem e conhecer outras features dela, dê uma olhada na nossa formação de Python para Web!