Python & AI Tutorials Logo
Programação Python

30. Introduzindo Classes e Objetos

30.1) A Ideia da Programação Orientada a Objetos (Criando Seus Próprios Tipos)

Ao longo deste livro, você vem trabalhando com os tipos embutidos do Python: inteiros, strings, listas, dicionários e mais. Cada tipo junta dados (como os caracteres em uma string) com operações que você pode realizar sobre esses dados (como .upper() ou .split()). Essa combinação de dados e comportamento é poderosa — ela permite que você pense em strings como entidades completas com suas próprias capacidades, e não só como sequências brutas de caracteres.

A programação orientada a objetos (OOP) amplia essa ideia: ela permite que você crie seus próprios tipos personalizados, chamados classes, que juntam dados e comportamento específicos do seu domínio de problema. Assim como o Python fornece um tipo str para trabalhar com texto e um tipo list para trabalhar com sequências, você pode criar um tipo BankAccount para gerenciar transações financeiras, um tipo Student para acompanhar registros acadêmicos ou um tipo Product para um sistema de inventário.

Por Que Criar Seus Próprios Tipos?

Considere gerenciar informações sobre estudantes em um sistema escolar. Sem classes, você poderia usar variáveis separadas ou dicionários:

python
# Usando variáveis separadas - vira bagunça rapidamente
student1_name = "Alice Johnson"
student1_id = "S12345"
student1_gpa = 3.8
 
student2_name = "Bob Smith"
student2_id = "S12346"
student2_gpa = 3.5
 
# Ou usando dicionários - melhor, mas ainda limitado
student1 = {"name": "Alice Johnson", "id": "S12345", "gpa": 3.8}
student2 = {"name": "Bob Smith", "id": "S12346", "gpa": 3.5}

Essa abordagem funciona para casos simples, mas ela tem limitações:

  1. Sem validação: Nada impede você de definir gpa com um valor inválido como -5.0 ou "excellent"
  2. Sem comportamento relacionado: Operações como calcular status de honra ou formatar informações do estudante são funções separadas espalhadas pelo seu código
  3. Sem verificação de tipo: Um dicionário que representa um estudante parece idêntico a qualquer outro dicionário — o Python não consegue ajudar você a detectar erros em que você acidentalmente usa um dicionário de produto quando era esperado um dicionário de estudante

Classes resolvem esses problemas ao permitir que você defina um novo tipo que representa exatamente o que é um estudante e quais operações fazem sentido para estudantes:

python
# Vamos construir até chegar nisso - uma classe Student que agrupa dados e comportamento
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def display_info(self):
        status = "Honors" if self.is_honors() else "Regular"
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa} [{status}]"
 
# Agora podemos criar objetos student
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.display_info())  # Output: Alice Johnson (S12345) - GPA: 3.8 [Honors]
print(bob.is_honors())       # Output: True

Este capítulo vai ensinar você a construir classes como essa do zero. Vamos começar com as classes mais simples possíveis e, aos poucos, adicionar recursos até que você consiga criar tipos personalizados ricos e úteis.

Classes vs Instâncias: A Analogia do Projeto

Entender a diferença entre uma classe e uma instância é fundamental na programação orientada a objetos:

  • Uma classe é como um projeto ou modelo. Ela define quais dados um tipo de objeto vai armazenar e quais operações ele pode realizar. A classe em si não é um estudante específico — ela é a definição do que significa ser um estudante.

  • Uma instância (também chamada de objeto) é um exemplo específico criado a partir desse projeto. Quando você cria alice = Student("Alice Johnson", "S12345", 3.8), você está criando uma instância específica de estudante com os dados particulares da Alice.

Classe Student
Projeto

instância alice
Nome: Alice Johnson
ID: S12345
GPA: 3.8

instância bob
Nome: Bob Smith
ID: S12346
GPA: 3.5

instância carol
Nome: Carol Davis
ID: S12347
GPA: 3.9

Você pode criar quantas instâncias precisar a partir de uma única classe, assim como um arquiteto pode usar um projeto para construir muitas casas. Cada instância tem seus próprios dados (o GPA da Alice é diferente do do Bob), mas todas compartilham a mesma estrutura e capacidades definidas pela classe.

O Que Você Vai Aprender Neste Capítulo

Este capítulo apresenta os conceitos centrais de programação orientada a objetos em Python:

  1. Definir classes com a palavra-chave class
  2. Criar instâncias e acessar seus atributos
  3. Adicionar métodos que operam sobre os dados da instância
  4. Entender self e como os métodos acessam dados da instância
  5. Inicializar instâncias com o método __init__
  6. Controlar representações em string com __str__ e __repr__
  7. Criar múltiplas instâncias independentes a partir da mesma classe

Ao final deste capítulo, você vai conseguir projetar e implementar seus próprios tipos personalizados que deixam seus programas mais organizados, fáceis de manter e expressivos. Vamos construir em cima dessas bases no Capítulo 31 com recursos mais avançados de classes, e no Capítulo 32 com herança e polimorfismo.

30.2) Definindo Classes Simples com class

Vamos começar criando a classe mais simples possível — uma que só define um novo tipo, sem dados nem comportamento ainda.

A Palavra-chave class

Você define uma classe usando a palavra-chave class, seguida do nome da classe e dois-pontos:

python
class Student:
    pass  # Classe vazia por enquanto
 
# Criar uma instância
alice = Student()
print(alice)  # Output: <__main__.Student object at 0x...>
print(type(alice))  # Output: <class '__main__.Student'>

Mesmo essa classe mínima é útil — ela cria um novo tipo chamado Student. Quando você cria uma instância com alice = Student(), o Python cria um novo objeto do tipo Student. A saída mostra que alice é de fato um objeto Student, embora ele ainda não faça nada de interessante.

Convenções de Nomeação de Classes

Os nomes de classes em Python seguem uma convenção específica chamada CapWords ou PascalCase: cada palavra começa com letra maiúscula, sem underscores entre as palavras:

python
class BankAccount:      # Bom: CapWords
    pass
 
class ProductInventory:  # Bom: CapWords
    pass
 
class HTTPRequest:      # Bom: a sigla HTTP fica em maiúsculas
    pass
 
# Evite estes estilos para classes:
# class bank_account:   # Wrong: snake_case é para funções/variáveis
# class bankaccount:    # Wrong: difícil de ler
# class BANKACCOUNT:    # Wrong: ALL_CAPS é para constantes

Essa convenção ajuda a distinguir classes de funções e variáveis (que usam snake_case) ao ler código.

Criando Instâncias

Criar uma instância a partir de uma classe parece chamar uma função — você usa o nome da classe seguido de parênteses:

python
class Product:
    pass
 
# Criar três instâncias diferentes de produto
item1 = Product()
item2 = Product()
item3 = Product()
 
# Cada instância é um objeto separado
print(item1)  # Output: <__main__.Product object at 0x...>
print(item2)  # Output: <__main__.Product object at 0x...>
print(item3)  # Output: <__main__.Product object at 0x...>
 
# Eles são objetos diferentes, mesmo sendo do mesmo tipo
print(item1 is item2)  # Output: False
print(type(item1) is type(item2))  # Output: True

Cada chamada a Product() cria uma nova instância independente. Os endereços de memória (a parte 0x...) são diferentes, confirmando que são objetos separados na memória.

Por Que Começar com Classes Vazias?

Talvez você se pergunte por que estamos começando com classes que não fazem nada. Há dois motivos:

  1. Clareza conceitual: Entender que uma classe é só um novo tipo, separado de seus dados e comportamento, ajuda você a pegar o conceito fundamental antes de adicionar complexidade.

  2. Uso prático: Mesmo classes vazias podem ser úteis como marcadores ou placeholders. Por exemplo, você pode definir tipos de exceção personalizados:

python
class InvalidGradeError:
    pass
 
class StudentNotFoundError:
    pass
 
# Essas classes vazias servem como tipos de erro distintos

No entanto, classes vazias são raras em código real. Vamos adicionar alguns dados para tornar nossas classes úteis.

30.3) Criando Instâncias e Acessando Atributos

Classes ficam úteis quando armazenam dados. Em Python, você pode adicionar atributos (dados ligados a uma instância) a qualquer momento simplesmente atribuindo valores a eles.

Adicionando Atributos às Instâncias

Você pode adicionar atributos a uma instância usando notação de ponto:

python
class Student:
    pass
 
# Criar uma instância
alice = Student()
 
# Adicionar atributos
alice.name = "Alice Johnson"
alice.student_id = "S12345"
alice.gpa = 3.8
 
# Acessar atributos
print(alice.name)        # Output: Alice Johnson
print(alice.student_id)  # Output: S12345
print(alice.gpa)         # Output: 3.8

O operador ponto (.) acessa atributos: alice.name significa “pegar o atributo name do objeto alice”. Essa é a mesma sintaxe que você vem usando com strings (como text.upper()) e listas (como numbers.append(5)) — isso acessa métodos e atributos desses objetos.

Cada Instância Tem Seus Próprios Atributos

Instâncias diferentes da mesma classe têm atributos independentes:

python
class Student:
    pass
 
# Criar dois estudantes
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# Cada instância tem seus próprios dados
print(alice.name)  # Output: Alice Johnson
print(bob.name)    # Output: Bob Smith
 
# Alterar uma não afeta a outra
alice.gpa = 3.9
print(alice.gpa)  # Output: 3.9
print(bob.gpa)    # Output: 3.5 (unchanged)

Essa independência é crucial: alice e bob são objetos separados com dados separados. Modificar alice.gpa não afeta bob.gpa.

Atributos Podem Ser de Qualquer Tipo

Atributos não ficam limitados a tipos simples — eles podem armazenar qualquer valor do Python:

python
class Student:
    pass
 
student = Student()
student.name = "Carol Davis"
student.grades = [95, 88, 92, 90]  # Atributo do tipo lista
student.contact = {                 # Atributo do tipo dicionário
    "email": "carol@example.com",
    "phone": "555-0123"
}
student.is_active = True            # Atributo do tipo booleano
 
# Acessar dados aninhados
print(student.grades[0])           # Output: 95
print(student.contact["email"])    # Output: carol@example.com

Essa flexibilidade permite que você modele entidades complexas do mundo real com estruturas de dados ricas.

Acessando Atributos Inexistentes

Tentar acessar um atributo que não existe levanta um AttributeError:

python
class Student:
    pass
 
student = Student()
student.name = "David Lee"
 
print(student.name)  # Output: David Lee
# print(student.age)  # AttributeError: 'Student' object has no attribute 'age'

Esse erro é útil — ele pega typos e erros de lógica em que você espera que um atributo exista, mas ele não existe.

O Problema com Atribuição Manual de Atributos

Embora você possa adicionar atributos manualmente depois de criar uma instância, essa abordagem tem desvantagens sérias:

python
class Student:
    pass
 
# É fácil esquecer atributos ou errar a grafia
alice = Student()
alice.name = "Alice Johnson"
alice.student_id = "S12345"
# Esqueceu de definir gpa!
 
bob = Student()
bob.name = "Bob Smith"
bob.stuent_id = "S12346"  # Typo: stuent em vez de student
bob.gpa = 3.5
 
# Agora alice está sem gpa, e bob tem um typo
# print(alice.gpa)  # AttributeError
# print(bob.student_id)  # AttributeError

Isso é propenso a erros e cansativo. Você precisa de uma forma de garantir que toda instância comece com os atributos corretos. É aí que entra o método __init__, que vamos ver na seção 30.5. Mas antes, vamos aprender sobre métodos — funções que pertencem a uma classe.

30.4) Adicionando Métodos de Instância: Entendendo self

Métodos são funções definidas dentro de uma classe que operam sobre os dados da instância. Eles dão comportamento às suas classes, não apenas dados.

Definindo um Método Simples

Vamos adicionar um método à nossa classe Student:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Criar uma instância e adicionar atributos
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Chamar o método
alice.display_info()  # Output: Alice Johnson - GPA: 3.8

O método display_info é definido dentro da classe usando def, assim como funções normais. A diferença-chave é o primeiro parâmetro: self.

Entendendo self

O parâmetro self é como um método acessa a instância específica sobre a qual está operando. Quando você chama alice.display_info(), o Python passa automaticamente alice como o primeiro argumento do método. Dentro do método, self se refere a alice, então self.name acessa alice.name e self.gpa acessa alice.gpa.

Veja o que acontece por baixo dos panos:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Essas duas chamadas são equivalentes:
alice.display_info()           # Jeito normal
Student.display_info(alice)    # O que o Python realmente faz
 
# Ambas imprimem: Alice Johnson - GPA: 3.8

Quando você escreve alice.display_info(), o Python traduz isso para Student.display_info(alice). A instância (alice) vira o parâmetro self dentro do método.

Por Que "self"?

O nome self é uma convenção, não uma palavra-chave. Tecnicamente, você poderia usar qualquer nome:

python
class Student:
    def display_info(this):  # Funciona, mas não faça isso
        print(f"{this.name} - GPA: {this.gpa}")

No entanto, sempre use self. É uma convenção universal do Python que deixa seu código legível para outros programadores Python. Usar qualquer outro nome confunde quem lê e viola os padrões da comunidade.

Métodos com Múltiplas Instâncias

O poder de self fica claro quando você tem várias instâncias:

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Criar dois estudantes
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# Mesmo método, dados diferentes
alice.display_info()  # Output: Alice Johnson - GPA: 3.8
bob.display_info()    # Output: Bob Smith - GPA: 3.5

Quando você chama alice.display_info(), self é alice. Quando você chama bob.display_info(), self é bob. O mesmo código do método funciona para qualquer instância porque self se adapta a qual instância o chamou.

alice.display_info

self = alice

bob.display_info

self = bob

Acessa alice.name
alice.gpa

Acessa bob.name
bob.gpa

Métodos Podem Receber Parâmetros Adicionais

Métodos podem aceitar parâmetros além de self:

python
class Student:
    def update_gpa(self, new_gpa):
        self.gpa = new_gpa
        print(f"Updated {self.name}'s GPA to {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
alice.update_gpa(3.9)  # Output: Updated Alice Johnson's GPA to 3.9
print(alice.gpa)       # Output: 3.9

Quando você chama alice.update_gpa(3.9), o Python passa alice como self e 3.9 como new_gpa. A assinatura do método é def update_gpa(self, new_gpa), mas você só passa um argumento ao chamá-lo — o Python cuida de self automaticamente.

Métodos Podem Retornar Valores

Métodos podem retornar valores como funções normais:

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def get_status(self):
        if self.is_honors():
            return "Honors Student"
        else:
            return "Regular Student"
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.2
 
print(alice.get_status())  # Output: Honors Student
print(bob.get_status())    # Output: Regular Student

Repare como get_status chama outro método (is_honors) usando self.is_honors(). Métodos podem chamar outros métodos na mesma instância.

Métodos vs Funções: Quando Usar Cada Um

Talvez você se pergunte quando usar um método versus uma função solta. Aqui vai a diretriz:

Use um método quando a operação:

  • Precisa acessar dados da instância (self.name, self.gpa, etc.)
  • Faz sentido pertencer ao tipo (é algo que um Student faz ou é)
  • Modifica o estado da instância

Use uma função solta quando a operação:

  • Não precisa de dados da instância
  • Funciona com múltiplos tipos
  • É uma utilidade geral
python
class Student:
    # Método: precisa de dados da instância
    def is_honors(self):
        return self.gpa >= 3.5
 
# Função: utilidade geral, funciona com qualquer valor de GPA
def calculate_letter_grade(gpa):
    if gpa >= 3.7:
        return "A"
    elif gpa >= 3.0:
        return "B"
    elif gpa >= 2.0:
        return "C"
    else:
        return "D"
 
alice = Student()
alice.gpa = 3.8
 
# Use o método para verificações específicas da instância
print(alice.is_honors())  # Output: True
 
# Use a função para cálculos gerais
print(calculate_letter_grade(alice.gpa))  # Output: A
print(calculate_letter_grade(2.5))        # Output: C

Padrões Comuns de Métodos

Aqui estão alguns padrões comuns que você vai usar com frequência:

Métodos getter (recuperam informação calculada):

python
class Student:
    def get_full_info(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"

Métodos setter (modificam atributos com validação):

python
class Student:
    def set_gpa(self, new_gpa):
        if 0.0 <= new_gpa <= 4.0:
            self.gpa = new_gpa
        else:
            print("Invalid GPA: must be between 0.0 and 4.0")

Métodos de consulta (respondem perguntas de sim/não):

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def is_failing(self):
        return self.gpa < 2.0

Métodos de ação (executam operações):

python
class Student:
    def add_grade(self, grade):
        self.grades.append(grade)
        # Recalcular o GPA com base em todas as notas
        self.gpa = sum(self.grades) / len(self.grades)

30.5) Inicializando Instâncias com __init__

Definir atributos manualmente depois de criar uma instância é cansativo e propenso a erros. O método __init__ resolve isso ao permitir que você inicialize instâncias com dados no momento em que elas são criadas.

O Método __init__

O método __init__ (pronunciado “dunder init” ou “init”) é um método especial que o Python chama automaticamente quando você cria uma nova instância:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# Criar instâncias com dados iniciais
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.name)  # Output: Alice Johnson
print(bob.gpa)     # Output: 3.5

Quando você escreve Student("Alice Johnson", "S12345", 3.8), o Python:

  1. Cria uma nova instância Student vazia
  2. Chama __init__ com essa instância como self e os seus argumentos
  3. Retorna a instância inicializada

O método __init__ não retorna explicitamente um valor — ele modifica a instância no lugar ao definir seus atributos. Se você tentar retornar um valor de __init__, o Python vai levantar um TypeError.

python
class Student:
    def __init__(self, name):
        self.name = name
        # Não retorne nada em __init__
        # return self  # Wrong! TypeError: __init__() should return None, not 'Student'

Como __init__ Funciona

Vamos detalhar o que acontece passo a passo:

python
class Student:
    def __init__(self, name, student_id, gpa):
        print(f"Initializing student: {name}")
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
        print(f"Initialization complete")
 
alice = Student("Alice Johnson", "S12345", 3.8)
# Output:
# Initializing student: Alice Johnson
# Initialization complete
 
print(alice.name)  # Output: Alice Johnson

Os parâmetros depois de self (name, student_id, gpa) viram argumentos obrigatórios ao criar uma instância. Se você não os fornecer, o Python levanta um TypeError:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# student = Student()  # TypeError: __init__() missing 3 required positional arguments
# student = Student("Alice")  # TypeError: __init__() missing 2 required positional arguments
student = Student("Alice Johnson", "S12345", 3.8)  # Correct

Isso é muito melhor do que atribuição manual de atributos — o Python garante que toda instância comece com os dados necessários.

Valores Padrão de Parâmetros em __init__

Você pode usar valores padrão de parâmetros em __init__, assim como em funções normais:

python
class Student:
    def __init__(self, name, student_id, gpa=0.0):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# GPA é opcional e por padrão é 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346")  # Usa o padrão gpa=0.0
 
print(alice.gpa)  # Output: 3.8
print(bob.gpa)    # Output: 0.0

Isso é útil para atributos que têm padrões sensatos, mas podem ser personalizados quando necessário.

Validação em __init__

Você pode validar a entrada em __init__ para garantir que instâncias comecem em um estado válido:

python
class Student:
    def __init__(self, name, student_id, gpa):
        if not name:
            print("Error: Name cannot be empty")
            self.name = "Unknown"
        else:
            self.name = name
        
        self.student_id = student_id
        
        if 0.0 <= gpa <= 4.0:
            self.gpa = gpa
        else:
            print(f"Warning: Invalid GPA {gpa}, setting to 0.0")
            self.gpa = 0.0
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice.gpa)  # Output: 3.8
 
bob = Student("", "S12346", 5.0)
# Output:
# Error: Name cannot be empty
# Warning: Invalid GPA 5.0, setting to 0.0
print(bob.name)  # Output: Unknown
print(bob.gpa)   # Output: 0.0

Isso garante que, mesmo se alguém passar dados inválidos, a instância termine em um estado razoável.

30.6) Representações em String com __str__ e __repr__

Quando você imprime uma instância com print() ou a vê no shell interativo, o Python precisa convertê-la para uma string. Por padrão, você obtém algo nada útil:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: <__main__.Student object at 0x...>

A saída padrão mostra o nome da classe e o endereço de memória, mas nada sobre os dados reais da Alice. Você pode personalizar isso com os métodos especiais __str__ e __repr__.

O Método __str__

O método __str__ define como suas instâncias são convertidas em strings por print() e str():

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: Alice Johnson (S12345) - GPA: 3.8
print(str(alice))  # Output: Alice Johnson (S12345) - GPA: 3.8

O método __str__ deve retornar uma string legível e informativa para usuários finais. Pense nela como a representação “amigável”.

O Método __repr__

O método __repr__ define a representação em string “oficial” das suas instâncias, usada pelo REPL e por repr():

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(repr(alice))  # Output: Student('Alice Johnson', 'S12345', 3.8)

No REPL:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)

O método __repr__ deve retornar uma string que pareça código Python válido para recriar o objeto. Pense nela como a representação “para desenvolvedor” — ela deve ser inequívoca e útil para debugging.

Usando __str__ e __repr__ Juntos

Você pode definir ambos os métodos para propósitos diferentes:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        # Formato amigável e legível
        return f"{self.name} - GPA: {self.gpa}"
    
    def __repr__(self):
        # Formato inequívoco, parecido com código
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
 
print(alice)        # Usa __str__
# Output: Alice Johnson - GPA: 3.8
 
print(repr(alice))  # Usa __repr__
# Output: Student('Alice Johnson', 'S12345', 3.8)

No REPL:

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice  # Uses __repr__
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice)  # Uses __str__
Alice Johnson - GPA: 3.8

Quando Definir Cada Método

Aqui vai a diretriz:

  • Sempre defina __repr__: Ele é usado pelo REPL e por ferramentas de debugging. Se você só definir um, defina este.
  • Defina __str__ quando você precisar de um formato amigável para usuários: Se sua classe vai ser impressa para usuários finais, forneça um __str__ legível.
  • Se você só definir __repr__: O Python o usa para repr(), e str() também cai para __repr__ (então print() também o usa).
  • Se você só definir __str__: print() usa __str__, mas repr() e o REPL usam o __repr__ padrão (mostrando o endereço de memória). Por isso, definir __repr__ normalmente é mais importante.
python
# Só __repr__ definido
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def __repr__(self):
        return f"Product('{self.name}', {self.price})"
 
item = Product("Laptop", 999.99)
print(item)        # Usa __repr__ como fallback
# Output: Product('Laptop', 999.99)
print(repr(item))  # Usa __repr__
# Output: Product('Laptop', 999.99)

Representação em String em Coleções

Quando instâncias estão dentro de coleções (listas, dicts, etc.), o Python usa __repr__ para exibi-las, não __str__:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name}: {self.gpa}"
    
    def __repr__(self):
        return f"Student('{self.name}', {self.gpa})"
 
students = [
    Student("Alice", 3.8),
    Student("Bob", 3.5),
    Student("Carol", 3.9)
]
 
# Imprimir a lista usa __repr__ para cada student
print(students)
# Output: [Student('Alice', 3.8), Student('Bob', 3.5), Student('Carol', 3.9)]
 
# Imprimir students individuais usa __str__
for student in students:
    print(student)
# Output:
# Alice: 3.8
# Bob: 3.5
# Carol: 3.9

Por isso __repr__ deve ser inequívoco — ele ajuda você a entender o que há nas suas estruturas de dados durante o debugging. Quando você imprime uma lista, o Python essencialmente chama repr() em cada elemento para mostrar a estrutura com clareza.

30.7) Criando Múltiplas Instâncias Independentes

Um dos aspectos mais poderosos das classes é que você pode criar muitas instâncias independentes, cada uma com seus próprios dados. Vamos explorar isso em profundidade.

Cada Instância Tem Seus Próprios Dados

Quando você cria múltiplas instâncias a partir da mesma classe, cada uma mantém seus próprios atributos separados:

python
class BankAccount:
    def __init__(self, account_number, holder_name, balance=0.0):
        self.account_number = account_number
        self.holder_name = holder_name
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
    
    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
            return True
        else:
            print(f"Insufficient funds. Balance: ${self.balance:.2f}")
            return False
    
    def __str__(self):
        return f"{self.holder_name}'s account ({self.account_number}): ${self.balance:.2f}"
 
# Criar três contas independentes
alice_account = BankAccount("ACC-001", "Alice Johnson", 1000.0)
bob_account = BankAccount("ACC-002", "Bob Smith", 500.0)
carol_account = BankAccount("ACC-003", "Carol Davis", 2000.0)
 
# Operações em uma conta não afetam as outras
alice_account.deposit(500)
# Output: Deposited $500.00. New balance: $1500.00
 
bob_account.withdraw(200)
# Output: Withdrew $200.00. New balance: $300.00
 
# Cada conta mantém seu próprio saldo
print(alice_account)  # Output: Alice Johnson's account (ACC-001): $1500.00
print(bob_account)    # Output: Bob Smith's account (ACC-002): $300.00
print(carol_account)  # Output: Carol Davis's account (ACC-003): $2000.00

Essa independência é fundamental para a programação orientada a objetos. Cada instância é uma entidade separada com seu próprio estado.

Instâncias em Coleções

Você pode armazenar instâncias em listas, dicionários ou qualquer outra coleção:

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
# Criar uma lista de students
students = [
    Student("Alice Johnson", "S12345", 3.8),
    Student("Bob Smith", "S12346", 3.2),
    Student("Carol Davis", "S12347", 3.9),
    Student("David Lee", "S12348", 3.4)
]
 
# Encontrar todos os students com honors
honors_students = []
for student in students:
    if student.is_honors():
        honors_students.append(student)
 
print("Honors students:")
for student in honors_students:
    print(f"  {student.name}: {student.gpa}")
# Output:
# Honors students:
#   Alice Johnson: 3.8
#   Carol Davis: 3.9
 
# Calcular o GPA médio
total_gpa = sum(student.gpa for student in students)
average_gpa = total_gpa / len(students)
print(f"Average GPA: {average_gpa:.2f}")  # Output: Average GPA: 3.58

Esse é um padrão comum: criar múltiplas instâncias, armazená-las em uma coleção e então processá-las com loops e compreensões.

Instâncias Podem Referenciar Outras Instâncias

Instâncias podem ter atributos que referenciam outras instâncias, criando relacionamentos entre objetos:

python
class Course:
    def __init__(self, course_code, course_name):
        self.course_code = course_code
        self.course_name = course_name
    
    def __str__(self):
        return f"{self.course_code}: {self.course_name}"
 
class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
        self.courses = []  # Lista de instâncias Course
    
    def enroll(self, course):
        self.courses.append(course)
        print(f"{self.name} enrolled in {course.course_name}")
    
    def list_courses(self):
        print(f"{self.name}'s courses:")
        for course in self.courses:
            print(f"  {course}")
 
# Criar courses
python_course = Course("CS101", "Introduction to Python")
data_course = Course("CS102", "Data Structures")
web_course = Course("CS103", "Web Development")
 
# Criar students e matriculá-los em courses
alice = Student("Alice Johnson", "S12345")
alice.enroll(python_course)
alice.enroll(data_course)
# Output:
# Alice Johnson enrolled in Introduction to Python
# Alice Johnson enrolled in Data Structures
 
bob = Student("Bob Smith", "S12346")
bob.enroll(python_course)
bob.enroll(web_course)
# Output:
# Bob Smith enrolled in Introduction to Python
# Bob Smith enrolled in Web Development
 
# Listar os courses de cada student
alice.list_courses()
# Output:
# Alice Johnson's courses:
#   CS101: Introduction to Python
#   CS102: Data Structures
 
bob.list_courses()
# Output:
# Bob Smith's courses:
#   CS101: Introduction to Python
#   CS103: Web Development

Repare que tanto Alice quanto Bob estão matriculados em python_course — eles estão referenciando a mesma instância de Course. Isso modela o relacionamento do mundo real em que múltiplos estudantes podem fazer o mesmo curso.

Identidade e Igualdade de Instância

Cada instância é um objeto único, mesmo que tenha os mesmos dados que outra instância:

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
 
alice1 = Student("Alice", 3.8)
alice2 = Student("Alice", 3.8)
 
# Objetos diferentes, mesmo com dados idênticos
print(alice1 is alice2)  # Output: False
print(id(alice1) == id(alice2))  # Output: False

Por padrão, == também verifica identidade (se são o mesmo objeto), não se têm os mesmos dados. No Capítulo 31, vamos aprender como personalizar a comparação de igualdade com o método especial __eq__.


Este capítulo apresentou para você os fundamentos da programação orientada a objetos em Python. Você aprendeu como definir classes, criar instâncias, adicionar métodos, inicializar instâncias com __init__, controlar representações em string e trabalhar com múltiplas instâncias independentes. Esses conceitos formam a base para recursos mais avançados de POO que vamos explorar nos Capítulos 31 e 32.


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