21 min de leitura
#11 - Orientação a Objetos em Python
Uma introdução prática à orientação a objetos em Python - classes, __init__, self, atributos, métodos, herança, polimorfismo, encapsulamento e classes internas.
Você já usou orientação a objetos em Python sem necessariamente perceber: toda string, toda lista, todo dicionário que você criou até agora é, por baixo dos panos, um objeto — instância de uma classe (str, list, dict) que já vem pronta na linguagem. Quando você chama minha_lista.append(1), está chamando um método de um objeto.
Neste post você vai aprender a criar suas próprias classes. É um post mais longo que os anteriores porque orientação a objetos é, de certa forma, um jeito diferente de organizar o raciocínio sobre um programa — vale a pena entender cada peça com calma: classes e objetos, o construtor __init__, o parâmetro self, atributos e métodos, herança, polimorfismo, encapsulamento e classes internas.
O que é orientação a objetos
Orientação a objetos (OOP, na sigla em inglês) é um jeito de organizar código em torno de classes e objetos, em vez de só funções soltas operando sobre dados soltos. Uma classe funciona como uma “planta” ou “molde” que descreve que atributos e comportamentos um tipo de objeto deve ter; um objeto é uma instância concreta construída a partir dessa planta.
Pense na classe Carro como o desenho técnico, e cada carro específico (seu Fusca vermelho, o Civic do seu vizinho) como um objeto — uma instância — construído a partir daquele desenho, cada um com sua própria cor, placa e quilometragem.
As vantagens de programar assim incluem:
- Deixa a estrutura do programa mais próxima de como pensamos sobre o problema do mundo real;
- Facilita manutenção, reuso e depuração de código;
- Segue o princípio DRY (“Don’t Repeat Yourself” — não se repita);
- Permite construir aplicações complexas combinando peças menores e reutilizáveis.
Criando classes e objetos
Uma classe é definida com a palavra-chave class. Por convenção, o nome da classe começa com letra maiúscula.
class Pessoa:
especie = "Homo sapiens"
Pra criar um objeto (uma instância) a partir dessa classe, você chama a classe como se fosse uma função:
class Pessoa:
especie = "Homo sapiens"
p1 = Pessoa()
print(p1.especie)
A saída é:
Homo sapiens
especie aqui é um atributo de classe — compartilhado por todos os objetos criados a partir de Pessoa, a menos que um objeto específico sobrescreva o próprio valor.
Classe vazia
Se você precisar de uma classe “esqueleto” por enquanto, sem nenhum atributo ainda, use pass:
class Placeholder:
pass
Apagando um objeto
Um objeto pode ser removido da memória com del:
p1 = Pessoa()
del p1
Depois disso, tentar usar p1 gera um erro, já que o objeto não existe mais.
O método init
Na prática, você quase nunca cria objetos com atributos totalmente fixos como no exemplo acima — o normal é que cada objeto tenha seus próprios valores, definidos no momento da criação. É pra isso que existe o __init__, o construtor da classe: um método especial, chamado automaticamente toda vez que você cria um novo objeto.
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
p1 = Pessoa("Ana", 28)
print(p1.nome)
print(p1.idade)
A saída é:
Ana
28
Sem o __init__, você precisaria atribuir cada atributo manualmente depois de criar o objeto (p1.nome = "Ana", p1.idade = 28) — o que funciona, mas é repetitivo e fácil de esquecer.
Valores padrão no construtor
Assim como em qualquer função, os parâmetros do __init__ podem ter valores padrão.
class Pessoa:
def __init__(self, nome, idade=18):
self.nome = nome
self.idade = idade
p1 = Pessoa("Carlos")
p2 = Pessoa("Bia", 32)
print(p1.nome, p1.idade)
print(p2.nome, p2.idade)
A saída é:
Carlos 18
Bia 32
O parâmetro self
Repare que todo método da classe, incluindo o __init__, recebe self como primeiro parâmetro. self é uma referência à instância atual — é através dele que um método acessa e modifica os atributos daquele objeto específico, e não de outro.
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def apresentar(self):
print(f"Eu sou {self.nome} e tenho {self.idade} anos.")
p1 = Pessoa("Ana", 28)
p2 = Pessoa("Bruno", 41)
p1.apresentar()
p2.apresentar()
A saída é:
Eu sou Ana e tenho 28 anos.
Eu sou Bruno e tenho 41 anos.
Sem self, não haveria como o método apresentar saber se deve usar os dados de p1 ou de p2 — é o self que amarra a chamada ao objeto certo.
Nota: o nome
selfnão é uma palavra reservada, é só uma convenção fortíssima — tecnicamente você poderia chamar o primeiro parâmetro de qualquer coisa (this,instancia), mas praticamente todo código Python do mundo usaself, e fugir disso só confunde quem for ler seu código.
Atributos (properties)
Atributos são as variáveis que guardam os dados de um objeto. Eles podem ser definidos no __init__ (atributos de instância, próprios de cada objeto) ou diretamente no corpo da classe (atributos de classe, compartilhados).
Acessando e modificando atributos
Atributos são acessados e alterados com a notação de ponto.
class Carro:
def __init__(self, marca, modelo):
self.marca = marca
self.modelo = modelo
meu_carro = Carro("Toyota", "Corolla")
print(meu_carro.marca, meu_carro.modelo)
meu_carro.modelo = "Corolla Cross"
print(meu_carro.modelo)
A saída é:
Toyota Corolla
Toyota Corolla Cross
Apagando um atributo
del meu_carro.modelo
print(meu_carro.marca)
Depois do del, tentar acessar meu_carro.modelo de novo gera um AttributeError — o atributo simplesmente não existe mais naquele objeto.
Adicionando atributos dinamicamente
Python permite adicionar um atributo novo a um objeto já criado, mesmo que ele não esteja no __init__. Isso afeta só aquele objeto específico, não a classe inteira nem os outros objetos.
meu_carro.ano = 2022
print(meu_carro.ano)
outro_carro = Carro("Honda", "Civic")
print(hasattr(outro_carro, "ano"))
A saída é:
2022
False
Métodos
Métodos são as funções definidas dentro de uma classe — eles descrevem o comportamento dos objetos. Você já viu um exemplo com apresentar() acima; vamos ver mais alguns padrões comuns.
Métodos com parâmetros
Métodos aceitam argumentos como qualquer função, só que sempre com self na frente.
class Calculadora:
def somar(self, a, b):
return a + b
def multiplicar(self, a, b):
return a * b
calc = Calculadora()
print(calc.somar(3, 4))
print(calc.multiplicar(3, 4))
A saída é:
7
12
Métodos que modificam o próprio objeto
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def fazer_aniversario(self):
self.idade += 1
p1 = Pessoa("Ana", 28)
p1.fazer_aniversario()
print(p1.idade)
A saída é:
29
O método especial str
Por padrão, imprimir um objeto direto mostra algo pouco útil, tipo <__main__.Pessoa object at 0x...>. Definindo __str__, você controla o que aparece quando o objeto é convertido pra texto (por print(), por exemplo).
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __str__(self):
return f"{self.nome} ({self.idade} anos)"
p1 = Pessoa("Ana", 28)
print(p1)
A saída é:
Ana (28 anos)
Herança
Herança permite criar uma classe nova a partir de uma classe já existente, reaproveitando (e podendo estender) tudo o que ela já tem. A classe original é chamada de classe pai (ou classe base); a nova é a classe filha (ou classe derivada).
class Pessoa:
def __init__(self, nome, sobrenome):
self.nome = nome
self.sobrenome = sobrenome
def nome_completo(self):
print(self.nome, self.sobrenome)
Criando a classe filha
Basta colocar a classe pai entre parênteses na definição da classe filha. Se a classe filha não definir nada de novo, ela já herda tudo do pai.
class Aluno(Pessoa):
pass
aluno1 = Aluno("João", "Silva")
aluno1.nome_completo()
A saída é:
João Silva
Sobrescrevendo o init com super()
Normalmente você quer que a classe filha tenha atributos próprios além dos herdados. Nesse caso, ela define seu próprio __init__, e usa super() pra chamar o __init__ da classe pai, sem precisar repetir a lógica dele.
class Aluno(Pessoa):
def __init__(self, nome, sobrenome, ano_formatura):
super().__init__(nome, sobrenome)
self.ano_formatura = ano_formatura
aluno1 = Aluno("João", "Silva", 2027)
aluno1.nome_completo()
print(aluno1.ano_formatura)
A saída é:
João Silva
2027
super().__init__(nome, sobrenome) delega a inicialização de nome e sobrenome pra classe Pessoa, deixando o __init__ de Aluno responsável só pelo que é específico dele (ano_formatura).
Adicionando e sobrescrevendo métodos
A classe filha pode adicionar métodos novos, ou redefinir um método herdado com um comportamento diferente — isso se chama sobrescrita (override).
class Aluno(Pessoa):
def __init__(self, nome, sobrenome, ano_formatura):
super().__init__(nome, sobrenome)
self.ano_formatura = ano_formatura
def nome_completo(self):
print(f"{self.nome} {self.sobrenome} (turma {self.ano_formatura})")
aluno1 = Aluno("João", "Silva", 2027)
aluno1.nome_completo()
A saída é:
João Silva (turma 2027)
Repare que nome_completo() aqui é uma versão nova, específica de Aluno — ela substitui, pra objetos dessa classe, o método original de Pessoa.
Polimorfismo
Polimorfismo significa “muitas formas”: a mesma operação (o mesmo nome de método, ou a mesma função embutida) se comporta de maneira diferente dependendo do tipo do objeto envolvido.
Polimorfismo em funções embutidas
Você já usa polimorfismo o tempo todo sem perceber. A função len(), por exemplo, funciona em tipos completamente diferentes, cada um com sua própria noção de “tamanho”:
print(len("Python"))
print(len([1, 2, 3, 4]))
print(len({"a": 1, "b": 2}))
A saída é:
6
4
2
Polimorfismo entre classes
Classes diferentes, sem relação de herança entre si, podem implementar um método de mesmo nome, cada uma com sua própria lógica.
class Carro:
def mover(self):
print("Dirigindo pela estrada")
class Barco:
def mover(self):
print("Navegando pela água")
class Aviao:
def mover(self):
print("Voando pelo céu")
for veiculo in (Carro(), Barco(), Aviao()):
veiculo.mover()
A saída é:
Dirigindo pela estrada
Navegando pela água
Voando pelo céu
O for chama veiculo.mover() sem se importar com o tipo exato de veiculo — cada objeto responde da sua própria maneira à mesma chamada.
Polimorfismo com herança
O caso mais comum na prática combina polimorfismo com herança: uma classe pai define um método, e cada classe filha sobrescreve esse método com seu próprio comportamento.
class Veiculo:
def __init__(self, marca, modelo):
self.marca = marca
self.modelo = modelo
def mover(self):
print("Se movendo...")
class Carro(Veiculo):
def mover(self):
print("Dirigindo!")
class Barco(Veiculo):
def mover(self):
print("Navegando!")
for veiculo in (Carro("Ford", "Ka"), Barco("Yamaha", "242X")):
print(veiculo.marca, veiculo.modelo)
veiculo.mover()
A saída é:
Ford Ka
Dirigindo!
Yamaha 242X
Navegando!
Encapsulamento
Encapsulamento é o princípio de proteger os dados internos de um objeto, controlando como código de fora pode acessar ou modificar esses dados — em vez de deixar tudo livremente exposto e sujeito a alterações indevidas.
Convenção de um underscore: atributo “protegido”
Um nome de atributo (ou método) começando com um único _ é uma convenção que sinaliza “isso é detalhe interno, não mexa direto de fora” — mas o Python não impede o acesso, é só um combinado entre programadores.
class Conta:
def __init__(self, titular, saldo):
self.titular = titular
self._saldo = saldo
Nada impede tecnicamente que alguém escreva conta._saldo = 999999 de fora da classe — é só uma convenção, não uma trava de verdade.
Dois underscores: atributo “privado”
Já um nome começando com dois underscores (__) sofre name mangling: o Python renomeia o atributo internamente (de __saldo para _Conta__saldo, por exemplo), o que dificulta bastante o acesso acidental de fora da classe.
class Conta:
def __init__(self, titular, saldo):
self.titular = titular
self.__saldo = saldo
def consultar_saldo(self):
return self.__saldo
def depositar(self, valor):
if valor > 0:
self.__saldo += valor
else:
print("Valor de depósito precisa ser positivo")
conta1 = Conta("Ana", 1000)
conta1.depositar(500)
print(conta1.consultar_saldo())
A saída é:
1500
Getters e setters
O padrão pra controlar o acesso a um atributo privado é expor métodos específicos pra ler (getter) e escrever (setter) o valor, aplicando validações no caminho.
class Conta:
def __init__(self, titular, saldo):
self.titular = titular
self.__saldo = saldo
def get_saldo(self):
return self.__saldo
def set_saldo(self, valor):
if valor >= 0:
self.__saldo = valor
else:
print("Saldo não pode ser negativo")
conta1 = Conta("Ana", 1000)
conta1.set_saldo(-50)
print(conta1.get_saldo())
A saída é:
Saldo não pode ser negativo
1000
Nota: encapsulamento em Python é mais uma questão de convenção e boas práticas do que uma trava rígida imposta pela linguagem — bem diferente de linguagens como Java, onde
privateé reforçado pelo compilador. Mesmo assim, vale seguir o padrão: use_ou__pra deixar claro o que é implementação interna, e exponha só o que realmente faz parte da interface pública da sua classe.
Classes internas (inner classes)
Uma classe interna é uma classe definida dentro de outra classe. Ela costuma fazer sentido quando um conceito só existe no contexto da classe externa, e não faz sentido usá-lo isoladamente.
class Carro:
def __init__(self, marca, modelo):
self.marca = marca
self.modelo = modelo
self.motor = self.Motor()
class Motor:
def ligar(self):
print("Motor ligado")
def desligar(self):
print("Motor desligado")
meu_carro = Carro("Fiat", "Argo")
meu_carro.motor.ligar()
meu_carro.motor.desligar()
A saída é:
Motor ligado
Motor desligado
Repare que, pra acessar a classe interna a partir de fora, você primeiro cria um objeto da classe externa (Carro) e depois acessa o atributo que guarda a instância da classe interna (meu_carro.motor).
Múltiplas classes internas
Uma classe externa pode agrupar mais de uma classe interna, quando faz sentido organizar assim.
class Computador:
def __init__(self):
self.cpu = self.CPU()
self.memoria = self.Memoria()
class CPU:
def processar(self):
print("Processando instruções")
class Memoria:
def armazenar(self):
print("Armazenando dados")
pc = Computador()
pc.cpu.processar()
pc.memoria.armazenar()
A saída é:
Processando instruções
Armazenando dados
Classes internas ajudam a organizar código relacionado, mas não abuse delas — na maioria dos casos, classes separadas no mesmo módulo já resolvem, e são mais simples de testar isoladamente.
E com isso você já tem a base de orientação a objetos em Python: classes e objetos, construtor e self, atributos e métodos, herança com super(), polimorfismo, encapsulamento e classes internas. É bastante conteúdo, mas é também a base de praticamente todo framework e biblioteca Python que você vai usar daqui pra frente.
Fonte adaptada: Python OOP, Python Classes/Objects, Python init() Function, Python self Parameter, Python Class Properties, Python Class Methods, Python Inheritance, Python Polymorphism, Python Encapsulation, Python Inner Classes
Comentários