Aula 17: Métodos Mágicos (Dunder Methods)

Personalizando o comportamento de seus objetos Python.

O Que Você Vai Aprender

Nesta aula, você descobrirá os poderosos Métodos Mágicos (também conhecidos como "Dunder Methods") em Python. Eles são a chave para fazer seus objetos personalizados se comportarem como os tipos de dados nativos do Python, como strings, listas e números, permitindo uma interação mais intuitiva e "pythônica".

✨ O Que São Dunder Methods

Compreender a finalidade e a sintaxe dos métodos mágicos.

🖨️ __str__ e __repr__

Aprender a controlar a representação de string de seus objetos para usuários e desenvolvedores.

📏 __len__ e __eq__

Implementar a funcionalidade de comprimento e comparação de igualdade para seus objetos.

🎯 Aplicações Práticas

Ver exemplos práticos de como esses métodos tornam o código mais legível e robusto.

Conceito: Métodos Mágicos (Dunder Methods)

Em Python, os "métodos mágicos" (também conhecidos como "métodos dunder" por causa do "double underscore", ou "duplo sublinhado", em seus nomes, como `__init__`, `__str__`) são métodos especiais predefinidos que você pode usar para adicionar "mágica" às suas classes.

Como Funcionam

Esses métodos são chamados automaticamente pelo interpretador Python em resposta a certas operações. Por exemplo:

  • Quando você usa a função `print()` em um objeto, o Python tenta chamar o método `__str__` desse objeto.
  • Quando você usa a função `len()` em um objeto, o Python tenta chamar o método `__len__`.
  • Quando você usa o operador `==` para comparar dois objetos, o Python tenta chamar o método `__eq__`.

Ao implementar esses métodos em suas classes, você pode definir como seus objetos se comportam em relação a essas operações embutidas, tornando suas classes mais intuitivas e alinhadas com o "estilo Python".

Image of Dunder Methods Concept

__str__ e __repr__: Representações de String

Esses dois métodos dunder são cruciais para controlar como seus objetos são representados como strings.

`__str__` (Para o Usuário)

Define a representação de string "informal" e legível por humanos de um objeto. É o que é retornado por `print(obj)`.

# Exemplo: __str__
class Livro:
    def __init__(self, titulo, autor):
        self.titulo = titulo
        self.autor = autor

    def __str__(self):
        # Representação legível para o usuário
        return f'"{self.titulo}" por {self.autor}'

# Uso:
# livro1 = Livro("O Pequeno Príncipe", "Antoine de Saint-Exupéry")
# print(livro1) # Chama __str__
# Saída: "O Pequeno Príncipe" por Antoine de Saint-Exupéry

`__repr__` (Para o Desenvolvedor)

Define a representação de string "oficial" de um objeto, que deve ser inequívoca e, se possível, permitir que o objeto seja recriado a partir dela. É o que é mostrado ao inspecionar um objeto no console ou em listas/dicionários.

# Exemplo: __repr__
class Livro:
    def __init__(self, titulo, autor):
        self.titulo = titulo
        self.autor = autor

    def __repr__(self):
        # Representação para desenvolvedor
        return f'Livro(titulo="{self.titulo}", autor="{self.autor}")'

# Uso:
# livro2 = Livro("1984", "George Orwell")
# print(repr(livro2)) # Chama __repr__ explicitamente
# Saída: Livro(titulo="1984", autor="George Orwell")
#
# Se __str__ não estiver definido, print() chamará __repr__.

__len__: Definindo o Comprimento

Ao implementar o método dunder `__len__`, você permite que a função embutida `len()` funcione com instâncias de sua classe, retornando o "comprimento" lógico do seu objeto.

Código Python para `__len__`

# Exemplo: __len__
class MinhaListaPersonalizada:
    def __init__(self, elementos):
        self.elementos = list(elementos) # Garante que é uma lista

    def adicionar(self, elemento):
        self.elementos.append(elemento)

    def __len__(self):
        # Define o comprimento do objeto como o comprimento da lista interna
        return len(self.elementos)

# Uso:
# minha_lista = MinhaListaPersonalizada([1, 2, 3])
# print(f"Comprimento da lista: {len(minha_lista)}") # Chama __len__
# Saída: Comprimento da lista: 3

# minha_lista.adicionar(4)
# print(f"Novo comprimento: {len(minha_lista)}") # Chama __len__ novamente
# Saída: Novo comprimento: 4

__eq__: Definindo a Igualdade

O método dunder `__eq__` é usado para definir o comportamento do operador de igualdade (`==`) entre dois objetos da sua classe. Por padrão, `==` compara a identidade (se são o mesmo objeto na memória), mas `__eq__` permite que você defina o que significa "ser igual" para seus objetos.

Código Python para `__eq__`

# Exemplo: __eq__
class Ponto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, outro):
        # Verifica se 'outro' é uma instância de Ponto
        if not isinstance(outro, Ponto):
            return NotImplemented # Indica que a comparação não é suportada

        # Define igualdade com base nos atributos x e y
        return self.x == outro.x and self.y == outro.y

    def __str__(self):
        return f"Ponto({self.x}, {self.y})"

# Uso:
# p1 = Ponto(1, 2)
# p2 = Ponto(1, 2)
# p3 = Ponto(3, 4)

# print(f"p1 == p2: {p1 == p2}") # Chama __eq__
# Saída: p1 == p2: True

# print(f"p1 == p3: {p1 == p3}") # Chama __eq__
# Saída: p1 == p3: False

# p4 = Ponto(1, 2) # Mesmo valor, mas outro objeto na memória
# print(f"p1 is p4: {p1 is p4}") # Compara identidade, Saída: False

# print(f"p1 == 'não um ponto': {p1 == 'não um ponto'}") # Saída: False ou erro, dependendo da implementação

Simulador Interativo: Métodos Mágicos

Experimente os métodos mágicos com um objeto `Produto` simulado em JavaScript. Veja como ele se comporta quando você tenta imprimi-lo, obter seu comprimento ou compará-lo com outro produto.

Código Python das Classes Simuladas

# Classe Produto com métodos mágicos (simulada em JS)
class Produto:
    def __init__(self, id, nome, preco, quantidade):
        self.id = id
        self.nome = nome
        self.preco = preco
        self.quantidade = quantidade

    def __str__(self):
        return f"{self.nome} (ID: {self.id}) - R${self.preco:.2f} [{self.quantidade} em estoque]"

    def __repr__(self):
        return f"Produto(id={self.id}, nome='{self.nome}', preco={self.preco}, quantidade={self.quantidade})"

    def __len__(self):
        return self.quantidade

    def __eq__(self, outro):
        if not isinstance(outro, Produto):
            return NotImplemented
        return self.id == outro.id and self.nome == outro.nome

Crie e Manipule Produtos

Produto 1

Produto 2

Ações nos Produtos

Resultados do simulador aparecerão aqui.

Desafios para Continuar

Agora é sua vez! Implemente esses métodos mágicos em suas próprias classes Python para entender melhor seu funcionamento.

  • 1. Classe `Playlist` com `__len__`, `__str__` e `__repr__`

    Crie uma classe `Musica` com `titulo` e `artista`. Em seguida, crie uma classe `Playlist` que contém uma lista de objetos `Musica`. Implemente:

    • `__len__`: Retorna o número de músicas na playlist.
    • `__str__`: Retorna uma string amigável para o usuário, listando o título da playlist e o número de músicas.
    • `__repr__`: Retorna uma string que permite recriar o objeto `Playlist`.
    # Exemplo de Estrutura:
    class Musica:
        def __init__(self, titulo, artista):
            self.titulo = titulo
            self.artista = artista
        def __repr__(self):
            return f"Musica(titulo='{self.titulo}', artista='{self.artista}')"
    
    class Playlist:
        def __init__(self, nome, musicas=None):
            self.nome = nome
            self.musicas = [] if musicas is None else list(musicas)
    
        def adicionar_musica(self, musica):
            self.musicas.append(musica)
    
        def __len__(self):
            return len(self.musicas)
    
        def __str__(self):
            return f"Playlist '{self.nome}' com {len(self)} músicas."
    
        def __repr__(self):
            musicas_repr = ', '.join(repr(m) for m in self.musicas)
            return f"Playlist(nome='{self.nome}', musicas=[{musicas_repr}])"
    
    # Teste:
    # musica1 = Musica("Bohemian Rhapsody", "Queen")
    # musica2 = Musica("Stairway to Heaven", "Led Zeppelin")
    # minha_playlist = Playlist("Rock Clássico", [musica1])
    # minha_playlist.adicionar_musica(musica2)
    # print(minha_playlist)
    # print(len(minha_playlist))
    # print(repr(minha_playlist))
                                
  • 2. Classe `Coordenada` com `__eq__`

    Crie uma classe `Coordenada` com atributos `x` e `y`. Implemente o método `__eq__` para que duas instâncias de `Coordenada` sejam consideradas iguais se seus atributos `x` e `y` forem iguais. Adicione também um `__str__` para uma boa representação visual.

    # Exemplo de Estrutura:
    class Coordenada:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __str__(self):
            return f"({self.x}, {self.y})"
    
        def __eq__(self, outro):
            if not isinstance(outro, Coordenada):
                return NotImplemented
            return self.x == outro.x and self.y == outro.y
    
    # Teste:
    # c1 = Coordenada(10, 20)
    # c2 = Coordenada(10, 20)
    # c3 = Coordenada(5, 15)
    # print(f"c1 == c2: {c1 == c2}")
    # print(f"c1 == c3: {c1 == c3}")