Capa do post

Dunders python, o que são? O que comem?

5/2/2022

Se você mexeu em um código Python que tinha classes, com certeza já viu uns métodos que iniciam e terminam com dois underscores, por exemplo, o método __init__ ou o método __str__, estes são os mais usados, o primeiro sendo quase que obrigatório.

Mas você sabe como funcionam esses métodos especiais, ou como são mais conhecidos, "métodos mágicos"?

Hoje vamos explorar um pouco esse lado dessa maravilhosa linguagem.

Estes tais métodos, como poucos talvez saibam, servem para especificar como prosseguir quando certas operações forem feitas.

Diferentemente de muitas linguagens, no Python você pode dizer como o interpretador deve prosseguir em uma adição, por exemplo.

Vejamos este exemplo:

a = 1
b = 2

print(a + b) # Vai imprimir o número 3, obviamente
print(a.__add__(b)) # Isso também vai funcionar e dar o mesmo resultado

Deu pra ter uma ideia de como funciona, certo?

Em Python, certas operações não são totalmente regidas pelo interpretador, como vimos nesse exemplo.

No primeiro exemplo o próprio Python foi lá em a e chamou o método add passando o b.

No segundo print a gente apenas chamou diretamente o método que a linguagem chamaria por baixo dos panos.

Agora vamos ver um exemplo em uma classe totalmente escrita pela gente.

class Peso:
    def __init__(self, quilos):
        self.quilos = quilos

Classe criada, vamos criar duas instancias dela e tentar somar uma com a outra:

a = Peso(50)
b = Peso(100)

print(a + b)

Provavelmente você esperava que mostrasse um lindo 150, certo? Mas tenho péssimas notícias, vamos na verdade ver algo tipo:

Traceback (most recent call last):
  File "main.py", line 9, in <module>
    print(a + b)
TypeError: unsupported operand type(s) for +: 'Peso' and 'Peso'

Agora vimos que não dá pra somar duas instâncias de classe assim sem definir o dunder add.

Uma opção seria fazer o seguinte:

print(a.quilos + b.quilos)

Funcionaria? Somaria "os pesos"? Sim e sim, mas estaríamos criando mais problemas que soluções, e provavelmente foi pensando nisso que adicionaram nosso querido dunder à linguagem.

Vamos refatorar e testar novamente nosso código.

Nosso script agora ta assim:

class Peso:
    def __init__(self, quilos):
        self.quilos = quilos
    
    def __add__(self, other):
        return self.quilos + other.quilos

a = Peso(50)
b = Peso(100)

print(a + b)

Dessa vez conseguimos somar os quilos de duas instâncias, mas ainda temos um problema, se fizermos:

print(a + 100)

Advinha o que acontece... Tempo para pensar...

Exatamente, teremos um erro parecido com esse:

Traceback (most recent call last):
  File "main.py", line 12, in <module>
    print(a + 100)
  File "main.py", line 7, in __add__
    return self.quilos + other.quilos
AttributeError: 'int' object has no attribute 'quilos'

Esse erro ocorre porque, como já vimos, por baixo dos panos o Python vai fazer a.__add__(100), mas em nossa classe precisamos do atributo quilos do parâmetro de add, mas aqui estamos passando um inteiro, que não contém tal atributo.

Parece tudo sem saída para você? Não se assuste pequeno gafanhoto, ainda podemos verificar os tipos antes de realizar a operação, ficando assim.

class Peso:
    def __init__(self, quilos):
        self.quilos = quilos
    
    def __add__(self, other):
        if isinstance(other, Peso):
            return self.quilos + other.quilos
        return self.quilos + other

Ali no primeiro if verificamos se o que queremos somar é uma instancia de Peso, se for, pegamos o atributo quilos como fizemos no início, se não, passamos direto e apenas somamos diretamente com o outro.

Nessa nova versão do nosso código, tanto print(a + b) quanto print(a + 100) vão retornar a mesma coisa.

Esse código pode dar erros se você for somar com outras coisas além dessas abordadas, mas aí fica como desafio para ti que está acompanhando até aqui.

Semelhantemente ao método add, temos também o radd.. Este é um método que apesar de "ser a mesma coisa" do primeiro, ele quase nunca é chamado, pois tem situações ainda mais específicas para ele entrar em cena.

Tomando nosso exemplo seguinte:

print(100 + a)

O que esperado? Uma soma como anteriormente, certo? Mas ao invés disso, temos um erro tipo o seguinte:

Traceback (most recent call last):
  File "main.py", line 15, in <module>
    print(100 + a)
TypeError: unsupported operand type(s) for +: 'int' and 'Peso'

Ali na última linha diz claramente que uma operação de adição entre um inteiro e Peso não é suportada. Mas por quê isso? Não deu certo antes? Tá dando erro apenas porque mudamos a ordem?

Sim, é porque a ordem ta inversa e porque o Python sempre chama primeiro o dunder add do primeiro valor, passando o segundo.

Não se desespere ainda, aqui entra nosso dunder radd.

O erro ocorreu também, porque em nossa classe Peso não o adicionamos. Se o tivéssemos lá, o próprio Python teria ignorado este erro da soma entre int e Peso, e teria chamado radd de peso. Vamos ver como funciona? É mais simples que piscar os olhos.

Nossa classe agora ta assim:

class Peso:
    def __init__(self, quilos):
        self.quilos = quilos
    
    def __add__(self, other):
        if isinstance(other, Peso):
            return self.quilos + other.quilos
        return self.quilos + other
    
    def __radd__(self, other):
        return self.__add__(other)

Sim, agora o Python entende aquela nossa operação.

print(100 + a)

# O Python vai tentar pegar o método add de 100 e passar o a, mas vai dar erro
# Depois desse erro, ele vai chamar o radd de a e passar o 100

Tudo certo até agora? Tudo entendido e sem bug algum? Fico feliz por isso.

Uma lista de dunders semelhantes:

  • sub e rsub são chamados com o operador -.
  • mul e rmul são chamados com o operador *.
  • matmul e rmatmul são chamados com o operador @.
  • truediv e rtruediv são chamados com o operador /.
  • floordiv e rfloordiv são chamados com o operador //.
  • mod e rmod são chamados com o operador %.
  • xor e rxor são chamados com o operador ^.

Ainda seguindo com alguns operadores aritméticos, vamos ver um exemplo comum mas que ainda menos pessoas sabem como funciona.

n1 = 1
print(n1)

Isso vai imprimir o mesmo número 1. Alguém surpreso com isso?

Agora vamos fazer uma soma diferente com a variável n1.

n1 += 3
print(n1)

O retorno? Sim, vai retornar o número 4, aqui somamos a variável n1 com o 3 e automaticamente já colocamos o valor disso em n1.

Vamos entender como funciona?

Por baixo dos panos, o que o Python realmente vai executar é o seguinte código:

n1 = n1.__iadd__(3)

Sim, temos mais um método mágico, novamente de adição, mas esse, diferentemente do __add__ com seu irmão __radd__, este não tem uma "variação", um "método irmão", não existe __riadd__.

Vamos ver como funciona na nossa classe Peso.

Com ainda aquele nosso código, vamos testar o seguinte código:

a += 100
print(a)

Oops, um erro? Não, na verdade a soma foi feita, e sabe por quê? Porque quando não especificamos nenhum método __iadd__, o próprio Python vai lá e chama um dunder que tenha a ver com a operação, que nesse caso serão: __add__ ou __radd__.

Também existe os seguintes:

  • isub para o operador -=.
  • imul para o operador *=.
  • imatmul para o operador @=.
  • itruediv para operador /=.
  • ifloordiv para o operador //=.
  • imod para o operador %=.
  • ixor para o operador ^=.

Saindo um pouco desses operadores aritméticos, vamos ver alguns outros também bastante úteis.

No dunder repr, como falado na própria documentação: "Chamado pela função embutida repr() para calcular a representação da string “oficial” de um objeto. Se possível, isso deve parecer uma expressão Python válida que pode ser usada para recriar um objeto com o mesmo valor (dado um ambiente apropriado)."

Por exemplo, se fizermos print(repr(a)), teremos uma saída parecida com:

<__main__.Peso object at 0x7f476fa92be0>

Aqui temos, o nome do ambiente onde a instância foi chamada, seguido do nome da classe instanciada e o seu "endereço em memória".

E se quisermos deixar isso mais bonitinho?

Vejamos nossa classe assim:

class Peso:
    def __init__(self, quilos):
        self.quilos = quilos
    
    def __add__(self, other):
        if isinstance(other, Peso):
            return self.quilos + other.quilos
        return self.quilos + other
    
    def __radd__(self, other):
        return self.__add__(other)
    
    def __repr__(self):
        return f'Peso({self.quilos})'

Agora se executarmos o mesmo print, teremos algo tipo Peso(50).

Semelhantemente temos o dunder str que é bem popular, ele é chamado quando vamos converter algum valor para string. Na nossa classe, se fizermos print(str(a)), vamos ter um retorno... igual ao anterior? Sim, exatamente isso. Isso ocorreu porque não especificamos um __str__, e quando ele não existe, o Python provavelmente vai chamar o __repr__ no seu lugar.

Se quisermos, que ao invés de ter o mesmo retorno de repr vejamos o atributo quilos convertido em string, vamos adicionar o seguinte trecho à nossa classe:

def __str__(self):
    return str(self.quilos)

Agora, fazendo o mesmo print(str(a)), vamos ver um '50'.

Simples e fácil, não?

Existem outros vários métodos, mas vamos ver só mais alguns em poucos detalhes, ok?

Métodos de comparação rica

  • a < b chama a.__lt__(b) — Menor que / Less Than
  • a <= b chama a.__le__(b) — Menor ou igual / Less or Equal
  • a == b chama a.__eq__(b) — Igual / Equals
  • a != b chama a.__ne__(b) — Não igual / Not Equal
  • a >= b chama a.__ge__(b) — Maior ou igual / Greather or Equal
  • a > b chama a.__gt__(b) — Maior que / Greather Than

Provavelmente já abordamos todos os métodos que você mais vai precisar usar, certo? Então por hoje é só.

Na nossa lista de leituras recomendadas de hoje, o primeiro link é da própria documentação do Python com todos os "métodos mágicos" disponíveis.

Leituras recomendadas

Onde me encontrar