Python & AI Tutorials Logo
Programação Python

31. Recursos Avançados de Classes

No Capítulo 30, aprendemos como criar classes básicas com atributos de instância e métodos. Agora vamos explorar recursos de classes mais sofisticados que dão a você um controle bem detalhado sobre como seus objetos se comportam. Esses recursos permitem criar classes que parecem tipos embutidos (built-in) do Python, com sintaxe natural para operações como adição, comparação e indexação.

31.1) Variáveis de Classe vs Variáveis de Instância

Quando criamos atributos em uma classe, temos dois lugares fundamentalmente diferentes para armazená-los: na própria classe ou em instâncias individuais. Entender essa distinção é essencial para escrever código orientado a objetos correto.

31.1.1) Entendendo Variáveis de Instância

Variáveis de instância (instance variables) são atributos que pertencem a um objeto específico. Cada instância tem sua própria cópia separada dessas variáveis. Usamos variáveis de instância ao longo do Capítulo 30 — são os atributos que criamos em __init__ usando self:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner      # Variável de instância
        self.balance = balance  # Variável de instância
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.balance)  # Output: 1000
print(account2.balance)  # Output: 500

Cada instância de BankAccount tem seu próprio owner e balance. Alterar account1.balance não afeta account2.balance — elas são completamente independentes.

31.1.2) Entendendo Variáveis de Classe

Variáveis de classe (class variables) são atributos que pertencem à própria classe, não a nenhuma instância em particular. Todas as instâncias compartilham a mesma variável de classe. Definimos variáveis de classe diretamente no corpo da classe, fora de qualquer método:

python
class BankAccount:
    interest_rate = 0.02  # Variável de classe - compartilhada por todas as instâncias
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
    
    def apply_interest(self):
        self.balance += self.balance * BankAccount.interest_rate
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
print(BankAccount.interest_rate)  # Output: 0.02

Perceba que podemos acessar interest_rate pelas instâncias (account1.interest_rate) ou pela própria classe (BankAccount.interest_rate). Ambos se referem à mesma variável.

Aqui está o que torna variáveis de classe poderosas — quando mudamos a variável de classe, todas as instâncias veem a mudança:

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
 
# Alterar a variável de classe
BankAccount.interest_rate = 0.03
 
print(account1.interest_rate)  # Output: 0.03
print(account2.interest_rate)  # Output: 0.03

As duas instâncias veem imediatamente a nova taxa de juros porque todas estão olhando para a mesma variável de classe.

31.1.3) A Armadilha do Shadowing: Quando Variáveis de Instância Escondem Variáveis de Classe

Aqui vai um comportamento sutil, mas importante: se você atribuir um atributo por meio de uma instância, o Python cria uma variável de instância que faz shadowing (esconde) a variável de classe:

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
# Criar uma variável de instância que faz shadowing da variável de classe
account1.interest_rate = 0.05
 
print(account1.interest_rate)  # Output: 0.05 (variável de instância)
print(account2.interest_rate)  # Output: 0.02 (variável de classe)
print(BankAccount.interest_rate)  # Output: 0.02 (variável de classe)

Agora account1 tem sua própria variável de instância interest_rate que esconde a variável de classe. A variável de classe ainda existe, mas account1.interest_rate passa a se referir à variável de instância. Normalmente isso não é o que você quer — se você precisa mudar uma variável de classe, mude-a pelo nome da classe, não por uma instância.

31.1.4) Usos Práticos para Variáveis de Classe

Variáveis de classe são úteis para dados que devem ser compartilhados entre todas as instâncias:

python
class Student:
    school_name = "Python High School"  # Igual para todos os alunos
    total_students = 0  # Acompanhar quantos alunos existem
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
        Student.total_students += 1  # Incrementar ao criar um aluno
    
    def __str__(self):
        return f"{self.name} (Grade {self.grade}) at {Student.school_name}"
 
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Carol", 10)
 
print(student1)  # Output: Alice (Grade 10) at Python High School
print(f"Total students: {Student.total_students}")  # Output: Total students: 3

Perceba como usamos Student.total_students (e não self.total_students) em __init__ para deixar claro que estamos modificando a variável de classe, não criando uma variável de instância.

Variáveis de Classe

Definidas no corpo da classe

Compartilhadas por todas as instâncias

Acessadas via ClassName.variable

Variáveis de Instância

Definidas em init com self

Únicas para cada instância

Acessadas via instance.variable

31.2) Gerenciando Atributos com @property

Às vezes você quer controlar o que acontece quando alguém acessa ou modifica um atributo. Por exemplo, você pode querer validar que um valor é positivo, ou calcular um valor sob demanda em vez de armazená-lo. O decorador @property do Python permite escrever métodos que parecem um acesso simples a um atributo.

31.2.1) O Problema: Acesso Direto a Atributos Não Consegue Validar

Quando atributos são acessados diretamente, você não consegue validar ou transformar os valores:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
 
# Nada impede que a gente defina temperaturas fisicamente impossíveis
temp.celsius = -500  # Abaixo do zero absoluto (-273.15°C)!
print(temp.celsius)  # Output: -500
 
# Ou valores absurdamente altos
temp.celsius = 1000000
print(temp.celsius)  # Output: 1000000

Sem validação, podemos acidentalmente definir dados inválidos, levando a bugs mais tarde no programa. Poderíamos usar métodos como get_celsius() e set_celsius(), mas isso não é idiomático em Python. Desenvolvedores Python esperam acessar atributos diretamente, não por métodos getter/setter como em Java ou C++.

31.2.2) Usando @property para Atributos Calculados

O decorador @property transforma um método em um "getter" acessado como um atributo:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        """Converter celsius para fahrenheit sob demanda"""
        return self.celsius * 9/5 + 32
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
print(temp.fahrenheit)  # Output: 77.0 (calculado, não armazenado)

Perceba que chamamos temp.fahrenheit sem parênteses — parece que estamos acessando um atributo, mas na verdade estamos chamando o método. O valor em fahrenheit é calculado toda vez que acessamos, então ele fica sempre sincronizado com celsius:

python
temp = Temperature(0)
print(temp.fahrenheit)  # Output: 32.0
 
temp.celsius = 100
print(temp.fahrenheit)  # Output: 212.0 (atualizado automaticamente)

31.2.3) Adicionando um Setter com @property_name.setter

Para permitir definir uma property, adicionamos um método setter usando o decorador @property_name.setter:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """Converter fahrenheit para celsius ao definir"""
        self.celsius = (value - 32) * 5/9
 
temp = Temperature(0)
print(temp.celsius)  # Output: 0
print(temp.fahrenheit)  # Output: 32.0
 
# Definir temperatura usando fahrenheit
temp.fahrenheit = 212
print(temp.celsius)  # Output: 100.0
print(temp.fahrenheit)  # Output: 212.0

O método setter recebe o novo valor e pode validá-lo ou transformá-lo antes de armazenar.

31.2.4) Usando Properties para Validação

Properties são excelentes para impor restrições:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance  # Underscore sugere "uso interno"
    
    @property
    def balance(self):
        """Obter o saldo atual"""
        return self._balance
    
    @balance.setter
    def balance(self, value):
        """Definir saldo, mas apenas se não-negativo"""
        if value < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = value
 
account = BankAccount("Alice", 1000)
print(account.balance)  # Output: 1000
 
account.balance = 1500  # Funciona normalmente
print(account.balance)  # Output: 1500
 
# Isso dispara um erro
account.balance = -100
# Output: ValueError: Balance cannot be negative

Perceba a convenção de nomes: armazenamos o valor real em _balance (com underscore no início) e o expomos pela property balance. O underscore é uma convenção em Python que sugere "isso é um detalhe interno de implementação", embora o atributo ainda seja tecnicamente acessível. Esse padrão permite controlar o acesso via property enquanto mantém o armazenamento real separado.

31.2.5) Properties Somente Leitura

Se você definir uma property sem um setter, ela vira somente leitura:

python
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        """Property calculada somente leitura"""
        return self.width * self.height
 
rect = Rectangle(5, 3)
print(rect.area)  # Output: 15
 
rect.width = 10
print(rect.area)  # Output: 30 (atualizado automaticamente)
 
# Tentar definir area dispara um erro
rect.area = 50
# Output: AttributeError: property 'area' of 'Rectangle' object has no setter

Isso é útil para valores derivados que devem ser calculados, não armazenados.

Decorador @property

Transforma método em getter

Acessado como atributo

Pode calcular valor sob demanda

@property_name.setter

Adiciona setter para a property

Pode validar antes de armazenar

Pode transformar valor

31.3) Métodos de Classe com @classmethod

Às vezes você precisa de métodos que trabalham com a própria classe em vez de com instâncias. Métodos de classe (class methods) recebem a classe como seu primeiro argumento (convencionalmente chamado de cls) em vez de uma instância (self).

31.3.1) Definindo Métodos de Classe

Criamos métodos de classe usando o decorador @classmethod:

python
class Student:
    school_name = "Python High School"
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    @classmethod
    def get_school_name(cls):
        """Método de classe - recebe a classe, não uma instância"""
        return cls.school_name
 
# Chamar na própria classe
print(Student.get_school_name())  # Output: Python High School
 
# Também pode chamar em uma instância (mas cls ainda é a classe)
student = Student("Alice", 10)
print(student.get_school_name())  # Output: Python High School

O parâmetro cls recebe automaticamente a classe, assim como self recebe automaticamente a instância em métodos normais.

31.3.2) Construtores Alternativos com Métodos de Classe

Um dos usos mais comuns para métodos de classe é criar construtores alternativos — formas diferentes de criar instâncias:

python
class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    @classmethod
    def from_string(cls, date_string):
        """Criar um Date a partir de uma string como '2024-12-27'"""
        year, month, day = date_string.split('-')
        return cls(int(year), int(month), int(day))
    
    @classmethod
    def today(cls):
        """Criar um Date para hoje (exemplo simplificado)"""
        # Em código real, você usaria o módulo datetime
        return cls(2024, 12, 27)
    
    def __str__(self):
        return f"{self.year}-{self.month:02d}-{self.day:02d}"
 
# Construtor normal
date1 = Date(2024, 12, 27)
print(date1)  # Output: 2024-12-27
 
# Construtor alternativo a partir de string
date2 = Date.from_string("2024-12-27")
print(date2)  # Output: 2024-12-27
 
# Construtor alternativo para hoje
date3 = Date.today()
print(date3)  # Output: 2024-12-27

Perceba como from_string e today retornam cls(...) — isso cria uma nova instância da classe. Usar cls em vez de colocar Date fixo faz com que o código funcione corretamente com subclasses (vamos aprender sobre herança no Capítulo 32).

31.3.3) Métodos de Classe para Padrões Factory

Métodos de classe são úteis para criar instâncias com configurações diferentes:

python
class DatabaseConnection:
    def __init__(self, host, port, database, username):
        self.host = host
        self.port = port
        self.database = database
        self.username = username
    
    @classmethod
    def for_development(cls):
        """Criar uma conexão configurada para desenvolvimento"""
        return cls("localhost", 5432, "dev_db", "dev_user")
    
    @classmethod
    def for_production(cls):
        """Criar uma conexão configurada para produção"""
        return cls("prod.example.com", 5432, "prod_db", "prod_user")
    
    def __str__(self):
        return f"Connection to {self.database} at {self.host}:{self.port}"
 
# Fácil criar conexões pré-configuradas
dev_conn = DatabaseConnection.for_development()
prod_conn = DatabaseConnection.for_production()
 
print(dev_conn)  # Output: Connection to dev_db at localhost:5432
print(prod_conn)  # Output: Connection to prod_db at prod.example.com:5432

31.3.4) Métodos de Classe para Contar Instâncias

Métodos de classe podem trabalhar com variáveis de classe para acompanhar informações sobre todas as instâncias:

python
class Product:
    total_products = 0
    
    def __init__(self, name, price):
        self.name = name
        self.price = price
        Product.total_products += 1
    
    @classmethod
    def get_total_products(cls):
        """Retornar o número total de produtos criados"""
        return cls.total_products
    
    @classmethod
    def reset_count(cls):
        """Resetar o contador de produtos"""
        cls.total_products = 0
 
product1 = Product("Laptop", 999)
product2 = Product("Mouse", 25)
product3 = Product("Keyboard", 75)
 
print(Product.get_total_products())  # Output: 3
 
Product.reset_count()
print(Product.get_total_products())  # Output: 0

31.4) Métodos Estáticos com @staticmethod

Métodos estáticos (static methods) são métodos que não recebem a instância (self) nem a classe (cls) como seu primeiro argumento. Eles são só funções comuns que, por acaso, são definidas dentro de uma classe porque têm relação lógica com aquela classe.

31.4.1) Definindo Métodos Estáticos

Criamos métodos estáticos usando o decorador @staticmethod:

python
class MathUtils:
    @staticmethod
    def is_even(number):
        """Verificar se um número é par"""
        return number % 2 == 0
    
    @staticmethod
    def is_prime(number):
        """Verificar se um número é primo (simplificado)"""
        if number < 2:
            return False
        for i in range(2, int(number ** 0.5) + 1):
            if number % i == 0:
                return False
        return True
 
# Chamar métodos estáticos na classe
print(MathUtils.is_even(4))  # Output: True
print(MathUtils.is_even(7))  # Output: False
print(MathUtils.is_prime(17))  # Output: True
print(MathUtils.is_prime(18))  # Output: False
 
# Também pode chamar em uma instância (mas é a mesma função)
utils = MathUtils()
print(utils.is_even(10))  # Output: True

Métodos estáticos não precisam de acesso a dados da instância ou da classe — eles são funções utilitárias autocontidas.

31.4.2) Quando Usar Métodos Estáticos vs Métodos de Classe vs Métodos de Instância

Aqui vai como escolher:

python
class Temperature:
    # Variável de classe
    absolute_zero_celsius = -273.15
    
    def __init__(self, celsius):
        self.celsius = celsius
    
    # Método de instância - precisa de acesso aos dados da instância (self)
    def to_fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    # Método de classe - precisa de acesso aos dados da classe (cls)
    @classmethod
    def get_absolute_zero(cls):
        return cls.absolute_zero_celsius
    
    # Método estático - não precisa de dados da instância nem da classe
    @staticmethod
    def celsius_to_kelvin(celsius):
        return celsius + 273.15
    
    @staticmethod
    def fahrenheit_to_celsius(fahrenheit):
        return (fahrenheit - 32) * 5/9
 
temp = Temperature(25)
 
# Método de instância - usa dados da instância
print(temp.to_fahrenheit())  # Output: 77.0
 
# Método de classe - usa dados da classe
print(Temperature.get_absolute_zero())  # Output: -273.15
 
# Métodos estáticos - apenas funções utilitárias
print(Temperature.celsius_to_kelvin(25))  # Output: 298.15
print(Temperature.fahrenheit_to_celsius(77))  # Output: 25.0

Diretrizes:

  • Use métodos de instância (instance methods) quando você precisa de acesso a atributos da instância (self)
  • Use métodos de classe (class methods) quando você precisa de acesso a atributos da classe ou quer construtores alternativos (cls)
  • Use métodos estáticos (static methods) quando você não precisa de acesso a dados da instância ou da classe, mas a função é logicamente relacionada à classe

Nota: Métodos estáticos poderiam ser funções independentes, mas colocá-los na classe agrupa funcionalidades relacionadas e evita poluir o namespace global.

Tipo de MétodoPrimeiro ParâmetroUse Quando
Método de InstânciaselfPrecisa de acesso a dados da instância
Método de ClasseclsPrecisa de acesso a dados da classe ou construtores alternativos
Método Estático(nenhum)Função utilitária relacionada à classe

31.4.3) Exemplo Prático: Utilitários de Validação

Métodos estáticos são ótimos para validação e funções utilitárias:

python
class User:
    def __init__(self, username, password):
        if not User.is_valid_username(username):
            raise ValueError("Invalid username")
        if not User.is_valid_password(password):
            raise ValueError("Invalid password")
        
        self.username = username
        self._password = password
    
    @staticmethod
    def is_valid_username(username):
        """Verificar se username atende aos requisitos"""
        return len(username) >= 3 and username.isalnum()
        
    @staticmethod
    def is_valid_password(password):
        """Verificar se password atende aos requisitos de segurança"""
        return len(password) >= 8 and any(c.isdigit() for c in password)
 
# Esses métodos de validação podem ser usados de forma independente
print(User.is_valid_username("alice123"))  # Output: True
print(User.is_valid_username("ab"))  # Output: False
print(User.is_valid_password("pass1234"))  # Output: True
 
# E podem ser usados em qualquer método da classe
try:
    user = User("ab", "short")
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: Invalid username

31.5) Entendendo Métodos Especiais (Magic Methods)

Os métodos especiais (special methods) (também chamados de magic methods ou dunder methods porque têm underscores duplos) permitem personalizar como seus objetos se comportam com as operações embutidas do Python. Já usamos __init__, __str__ e __repr__ no Capítulo 30. Agora vamos explorar muitos outros.

31.5.1) O Que Métodos Especiais Fazem

Métodos especiais são chamados automaticamente pelo Python quando você usa certa sintaxe ou funções embutidas:

python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Point({self.x}, {self.y})"
 
point = Point(3, 4)
 
# Quando você chama print(), o Python chama __str__()
print(point)  # Output: Point(3, 4)
# Isso é equivalente a: print(point.__str__())

Métodos especiais permitem fazer suas classes se comportarem como tipos embutidos. Por exemplo, você pode fazer seus objetos:

  • Suportarem operações aritméticas (+, -, *, /)
  • Serem comparáveis (<, >, ==)
  • Funcionarem com len(), in e indexação
  • Agirem como containers ou sequências

31.5.2) Categorias Comuns de Métodos Especiais

Aqui estão as principais categorias de métodos especiais:

Representação em String (como objetos são exibidos):

  • __str__() - para print() e str()
  • __repr__() - para o REPL e repr()

Comparação (comparar objetos):

  • __eq__() - para ==
  • __ne__() - para !=
  • __lt__() - para <
  • __le__() - para <=
  • __gt__() - para >
  • __ge__() - para >=

Aritmética (operações matemáticas):

  • __add__() - para +
  • __sub__() - para -
  • __mul__() - para *
  • __truediv__() - para /

Container/Sequência (comportamento tipo coleção):

  • __len__() - para len()
  • __contains__() - para in
  • __getitem__() - para indexação obj[key]
  • __setitem__() - para atribuição obj[key] = value

Vamos explorar isso em detalhes nas próximas seções.

31.6) Exemplo 1: Interface de Coleção (len, contains)

Vamos criar uma classe que gerencia uma coleção de itens e fazer com que ela funcione com a função embutida len() e com o operador in.

31.6.1) Implementando len para len()

O método especial __len__() é chamado quando você usa len() no seu objeto:

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        """Retornar a quantidade de itens no carrinho"""
        return len(self.items)
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
cart.add_item("Orange")
 
# len() chama __len__()
print(len(cart))  # Output: 3

Sem __len__(), chamar len(cart) dispararia um TypeError. Ao implementá-lo, nosso ShoppingCart funciona como coleções embutidas.

31.6.2) Implementando contains para o Operador in

O método especial __contains__() é chamado quando você usa o operador in:

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        return len(self.items)
    
    def __contains__(self, item):
        """Verificar se um item está no carrinho"""
        return item in self.items
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
 
# O operador in chama __contains__()
print("Apple" in cart)  # Output: True
print("Orange" in cart)  # Output: False

Agora nosso carrinho suporta a sintaxe natural do Python para testar pertencimento.

31.6.3) Construindo uma Classe de Coleção Mais Completa

Vamos criar uma classe de coleção mais realista que acompanha notas de estudantes:

python
class GradeBook:
    def __init__(self):
        self.grades = {}  # student_name: list of grades
    
    def add_grade(self, student, grade):
        """Adicionar uma nota para um aluno"""
        if student not in self.grades:
            self.grades[student] = []
        self.grades[student].append(grade)
    
    def __len__(self):
        """Retornar o número de alunos"""
        return len(self.grades)
    
    def __contains__(self, student):
        """Verificar se um aluno tem alguma nota"""
        return student in self.grades
    
    def get_average(self, student):
        """Obter a média de um aluno"""
        if student not in self:
            return None
        grades = self.grades[student]
        return sum(grades) / len(grades)
    
    def __str__(self):
        return f"GradeBook with {len(self)} students"
 
gradebook = GradeBook()
gradebook.add_grade("Alice", 85)
gradebook.add_grade("Alice", 90)
gradebook.add_grade("Bob", 78)
gradebook.add_grade("Bob", 82)
gradebook.add_grade("Bob", 88)
 
print(gradebook)  # Output: GradeBook with 2 students
print(len(gradebook))  # Output: 2
 
print("Alice" in gradebook)  # Output: True
print("Carol" in gradebook)  # Output: False
 
print(f"Alice's average: {gradebook.get_average('Alice')}")  # Output: Alice's average: 87.5
print(f"Bob's average: {gradebook.get_average('Bob')}")  # Output: Bob's average: 82.66666666666667

Perceba como get_average() usa if student not in self — isso chama nosso método __contains__(), fazendo o código ficar natural de ler.

31.7) Exemplo 2: Sobrecarga de Operadores (add, eq, lt)

Sobrecarga de operadores (operator overloading) significa definir o que operadores como +, == e < fazem para suas classes personalizadas. Isso faz seus objetos funcionarem de forma natural com a sintaxe do Python.

31.7.1) Implementando add para Adição

O método especial __add__() é chamado quando você usa o operador +:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """Somar dois vetores"""
        return Vector(self.x + other.x, self.y + other.y)
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
# O operador + chama __add__()
v3 = v1 + v2
print(v3)  # Output: Vector(4, 6)

Quando o Python vê v1 + v2, ele chama v1.__add__(v2). O método __add__() do operando da esquerda recebe o operando da direita como argumento.

31.7.2) Implementando eq para Igualdade

O método especial __eq__() é chamado quando você usa o operador ==:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __eq__(self, other):
        """Verificar se dois vetores são iguais"""
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
 
# O operador == chama __eq__()
print(v1 == v2)  # Output: True
print(v1 == v3)  # Output: False

Sem __eq__(), o Python compara a identidade do objeto (se é o mesmo objeto na memória), não seus valores. Com __eq__(), definimos o que significa igualdade para a nossa classe.

31.7.3) Implementando Operadores de Comparação

Vamos implementar operadores de comparação para uma classe Money:

python
class Money:
    def __init__(self, amount):
        self.amount = amount
    
    def __eq__(self, other):
        """Verificar se os valores são iguais"""
        return self.amount == other.amount
    
    def __lt__(self, other):
        """Verificar se este valor é menor que o outro"""
        return self.amount < other.amount
    
    def __le__(self, other):
        """Verificar se este valor é menor ou igual ao outro"""
        return self.amount <= other.amount
    
    def __gt__(self, other):
        """Verificar se este valor é maior que o outro"""
        return self.amount > other.amount
    
    def __ge__(self, other):
        """Verificar se este valor é maior ou igual ao outro"""
        return self.amount >= other.amount
    
    def __str__(self):
        return f"${self.amount:.2f}"
 
price1 = Money(10.50)
price2 = Money(15.75)
price3 = Money(10.50)
 
print(price1 == price3)  # Output: True
print(price1 < price2)  # Output: True
print(price1 <= price3)  # Output: True
print(price2 > price1)  # Output: True
print(price2 >= price1)  # Output: True

31.7.4) Lidando com Incompatibilidade de Tipos em Operadores

Ao implementar operadores, você deve lidar com casos em que o outro operando não é do tipo esperado:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """Somar dois vetores ou somar um escalar aos dois componentes"""
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        elif isinstance(other, (int, float)):
            return Vector(self.x + other, self.y + other)
        else:
            return NotImplemented  # Deixar o Python tentar other.__radd__(self)
    
    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
print(v1 + v2)  # Output: Vector(4, 6) (adição de vetores)
print(v1 + 5)  # Output: Vector(6, 7) (adição escalar)
 
print(v1 == v2)  # Output: False
print(v1 == "not a vector")  # Output: False (sem erro)

Retornar NotImplemented (uma constante especial embutida) diz ao Python para tentar a operação refletida no outro operando. Isso é importante para fazer operadores funcionarem corretamente com tipos diferentes.

Sobrecarga de Operadores

Operadores Aritméticos

Operadores de Comparação

add para +

sub para -

mul para *

truediv para /

eq para ==

lt para <

le para <=

gt para >

ge para >=

31.8) Exemplo 3: Acesso a Sequência (getitem, setitem)

Os métodos especiais __getitem__() e __setitem__() permitem usar a sintaxe de indexação (obj[key]) com suas classes personalizadas. Isso faz seus objetos se comportarem como listas, dicionários ou outras sequências.

31.8.1) Implementando getitem para Indexação

O método __getitem__() é chamado quando você usa colchetes para acessar um item:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """Obter uma música pelo índice"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# A indexação chama __getitem__()
print(playlist[0])  # Output: Song A
print(playlist[1])  # Output: Song B
print(playlist[-1])  # Output: Song C (indexação negativa funciona!)

Como delegamos para self.songs[index], todos os recursos de indexação de lista funcionam automaticamente: índices positivos, índices negativos e até levantar IndexError para índices inválidos.

31.8.2) Suportando Slicing com getitem

O mesmo método __getitem__() também lida com slicing:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """Obter uma música pelo índice ou slice"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
 
# O slicing também chama __getitem__()
print(playlist[1:3])  # Output: ['Song B', 'Song C']
print(playlist[:2])  # Output: ['Song A', 'Song B']
print(playlist[::2])  # Output: ['Song A', 'Song C']

Quando você usa slicing, o Python passa um objeto slice para __getitem__(). Ao delegar para self.songs[index], damos suporte automaticamente a toda a sintaxe de slice.

31.8.3) Implementando setitem para Atribuição

O método __setitem__() é chamado quando você atribui a um índice:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __setitem__(self, index, value):
        """Substituir uma música em um índice específico"""
        self.songs[index] = value
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
print(playlist[1])  # Output: Song B
 
# A atribuição chama __setitem__()
playlist[1] = "New Song B"
print(playlist[1])  # Output: New Song B

31.8.4) Tornando Objetos Iteráveis com getitem

Um efeito colateral interessante: se você implementar __getitem__() com índices inteiros começando em 0, seu objeto automaticamente vira iterável:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# Loops for funcionam automaticamente!
for song in playlist:
    print(song)
# Output:
# Song A
# Song B
# Song C

O Python tenta iterar chamando __getitem__(0), depois __getitem__(1), e assim por diante, até receber um IndexError. Esse é um protocolo de iteração mais antigo — vamos aprender sobre o protocolo moderno de iterador no Capítulo 35.

31.8.5) Acesso Tipo Dicionário com Chaves String

__getitem__() e __setitem__() funcionam com qualquer tipo de chave, não apenas inteiros:

python
class ScoreBoard:
    def __init__(self):
        self.scores = {}
    
    def __getitem__(self, player_name):
        """Obter a pontuação de um jogador"""
        return self.scores.get(player_name, 0)
    
    def __setitem__(self, player_name, score):
        """Definir a pontuação de um jogador"""
        self.scores[player_name] = score
    
    def __contains__(self, player_name):
        return player_name in self.scores
    
    def __len__(self):
        return len(self.scores)
 
scoreboard = ScoreBoard()
 
# Definir pontuações usando chaves string
scoreboard["Alice"] = 100
scoreboard["Bob"] = 85
 
# Atualizar uma pontuação
scoreboard["Alice"] = 120
 
# Obter pontuações
print(scoreboard["Alice"])  # Output: 120
print(scoreboard["Bob"])    # Output: 85
print(scoreboard["Carol"])  # Output: 0
 
print("Alice" in scoreboard)  # Output: True
print(len(scoreboard))  # Output: 2

Acesso a Sequência

getitem

setitem

Chamado para obj[key]

Lida com indexação

Lida com slicing

Torna o objeto iterável

Chamado para obj[key] = value

Habilita atribuição

Pode validar valores


Este capítulo mostrou como criar classes sofisticadas que se integram perfeitamente à sintaxe do Python. Ao implementar variáveis de classe, properties, métodos de classe, métodos estáticos e métodos especiais, você pode fazer suas classes personalizadas se comportarem como tipos embutidos. No Capítulo 32, vamos explorar herança e polimorfismo, que permitem construir hierarquias de classes relacionadas que compartilham e estendem comportamento.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai