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:
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: 500Cada 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:
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.02Perceba 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:
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.03As 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:
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:
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: 3Perceba 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.
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:
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: 1000000Sem 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:
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:
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:
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.0O 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:
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 negativePerceba 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:
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 setterIsso é útil para valores derivados que devem ser calculados, não armazenados.
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:
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 SchoolO 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:
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-27Perceba 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:
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:543231.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:
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: 031.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:
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: TrueMé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:
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.0Diretrizes:
- 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étodo | Primeiro Parâmetro | Use Quando |
|---|---|---|
| Método de Instância | self | Precisa de acesso a dados da instância |
| Método de Classe | cls | Precisa 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:
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 username31.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:
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(),ine 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__()- paraprint()estr()__repr__()- para o REPL erepr()
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__()- paralen()__contains__()- parain__getitem__()- para indexaçãoobj[key]__setitem__()- para atribuiçãoobj[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:
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: 3Sem __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:
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: FalseAgora 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:
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.66666666666667Perceba 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 +:
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 ==:
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: FalseSem __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:
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: True31.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:
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.
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:
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:
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:
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 B31.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:
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 CO 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:
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: 2Este 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.