Programação orientada a objetos (POO) é o jeito de organizar um programa em torno de objetos, que juntam num só lugar os dados e as funções que mexem nesses dados. É o passo que separa quem escreve scripts curtos de quem consegue tocar um projeto maior sem se perder. Este guia mostra a POO em Python na prática, do conceito de classe até dataclasses e MRO, sempre com código que roda: classe e objeto, self, encapsulamento, herança, polimorfismo, abstração, propriedades, métodos mágicos e os erros mais comuns de quem começa. Se quiser estudar com playground no navegador, exercícios corrigidos e certificado, use o curso de Python intermediário, gratuito.
Resposta rápida
- Classe é o molde; objeto é a peça criada a partir dele.
__init__monta o objeto eselfé o próprio objeto dentro dos métodos. - Os quatro pilares em Python: encapsulamento, herança (com
super()), polimorfismo (duck typing) e abstração (comABC). - O encapsulamento aqui é por convenção: um
_sinaliza uso interno e__só faz name mangling. Python não tem atributo privado de verdade. - Python não obriga a usar POO. Para script simples, função basta; classe vale quando dados e comportamento andam juntos.
O problema que a POO resolve
Imagine controlar uma conta bancária com variáveis soltas. O saldo fica numa variável, e você escreve funções separadas para depositar e sacar, passando o saldo toda hora. Com uma conta só já dá trabalho. Com dez, o código vira um emaranhado de variáveis parecidas (saldo1, saldo2, dono1) e ninguém garante que a função certa mexe no saldo certo. A orientação a objetos resolve isso juntando o dado (o saldo) e o comportamento (depositar, sacar) numa mesma unidade que sabe cuidar de si mesma.
class Conta:
def __init__(self, dono, saldo=0):
self.dono = dono
self.saldo = saldo
def depositar(self, valor):
self.saldo += valor
def sacar(self, valor):
if valor > self.saldo:
print("Saldo insuficiente")
else:
self.saldo -= valor
conta = Conta("Ana", 100)
conta.depositar(50)
conta.sacar(30)
print(conta.saldo) # 120Toda a lógica da conta mora dentro da classe. Quem usa não precisa saber como o saldo é guardado por dentro: chama depositar e sacar e pronto. É a mesma ideia por trás das listas e outros tipos do Python, que também juntam dados e métodos num objeto que você só usa.
Classe e objeto: a planta e a casa
A confusão mais comum de quem começa é misturar classe com objeto. A classe é a planta da casa: um desenho no papel. O objeto é a casa construída a partir da planta. De uma planta você constrói quantas casas quiser, cada uma com sua cor de parede e seus moradores, mas todas seguindo o mesmo desenho.
class Cachorro:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def latir(self):
return f"{self.nome} diz: Au!"
rex = Cachorro("Rex", 3)
bob = Cachorro("Bob", 5)
print(rex.latir()) # Rex diz: Au!
print(bob.idade) # 5O rex e o bob são dois objetos (ou instâncias) da mesma classe Cachorro. Cada um guarda o próprio nome e a própria idade. O método __init__ é o construtor: ele roda no momento em que você escreve Cachorro("Rex", 3) e prepara o objeto com os valores iniciais. E o self? Ele é o próprio objeto, entregue de graça a cada método. Quando você chama rex.latir(), o Python passa o rex como self por baixo dos panos. Por isso self.nome dentro do método sabe que é o nome do rex, e não do bob.
Atributo de instância x atributo de classe
Existem dois lugares onde um dado pode morar. O atributo de instância nasce dentro do __init__, com self.algo, e é individual: cada objeto tem o seu. O atributo de classe é escrito direto no corpo da classe, fora dos métodos, e é compartilhado por todos os objetos. Serve para valores iguais para todos, como uma taxa padrão ou o número de patas de um cachorro.
Aqui mora uma das armadilhas mais famosas de Python. Se o atributo de classe for uma lista ou um dicionário (um objeto mutável), todos os objetos dividem o mesmo:
class Cachorro:
patas = 4 # atributo de classe, ok: valor fixo
truques = [] # ARMADILHA: lista compartilhada por todos
def __init__(self, nome):
self.nome = nome
def ensinar(self, truque):
self.truques.append(truque)
rex = Cachorro("Rex")
bob = Cachorro("Bob")
rex.ensinar("sentar")
print(bob.truques) # ['sentar'] <- vazou para o bob!O rex aprendeu a sentar, mas o truque apareceu na lista do bob. Isso acontece porque truques é um único objeto lista, e self.truques.append alterou essa lista comum. A correção é criar a lista dentro do __init__, com self.truques = [], para que cada cachorro ganhe a sua. Regra de bolso: dado que varia por objeto vai sempre no construtor.
Os quatro pilares em Python
A POO costuma ser resumida em quatro ideias. Não decore os nomes: entenda o problema que cada uma resolve. As próximas seções mostram cada pilar com código de verdade.
| Pilar | O que resolve | Ferramenta em Python |
|---|---|---|
| Encapsulamento | Manter dados e comportamento juntos, protegidos de mexida indevida | _nome, __nome, @property |
| Herança | Reaproveitar uma classe em outra parecida | class Filha(Pai), super() |
| Polimorfismo | Tratar objetos diferentes pela mesma interface | duck typing, métodos com o mesmo nome |
| Abstração | Esconder o detalhe e expor só o que importa usar | ABC, @abstractmethod |
Encapsulamento: convenção, não cadeado
No primeiro exemplo, alguém poderia escrever conta.saldo = -999 e furar a regra do saldo. Encapsular é impedir isso, guardando o dado por dentro e oferecendo métodos que controlam o acesso. Aqui está a parte que confunde quem vem do Java: Python não tem atributo privado de verdade. O controle é por combinação, não por trava da linguagem.
A convenção tem dois níveis. Um sublinhado à frente (_saldo) é um recado: uso interno, não mexa direto. A linguagem não impede nada, mas a comunidade respeita. Dois sublinhados (__saldo) ligam o name mangling, um mecanismo que renomeia o atributo para evitar colisão em herança:
class Conta:
def __init__(self, saldo):
self.__saldo = saldo # name mangling: vira _Conta__saldo
c = Conta(100)
# print(c.__saldo) # AttributeError
print(c._Conta__saldo) # 100 <- ainda dá para acessarRepare: o __saldo não sumiu, ele só foi renomeado para _Conta__saldo. Quem souber o nome ainda alcança o valor. O objetivo dos dois sublinhados nunca foi esconder de verdade, e sim evitar que uma classe filha sobrescreva sem querer um atributo do pai. Se você veio de outra linguagem esperando private, ajuste a expectativa: em Python o encapsulamento é um acordo entre pessoas adultas, não uma cerca.
@property: getter e setter pythônico
Se o dado é interno, como validar antes de gravar sem obrigar quem usa a chamar set_saldo() o tempo todo? A resposta é a @property. Ela transforma um método em algo que se usa como se fosse um atributo simples, e deixa você interceptar leitura e escrita:
class Produto:
def __init__(self, preco):
self._preco = preco
@property
def preco(self):
return self._preco
@preco.setter
def preco(self, valor):
if valor < 0:
raise ValueError("Preco nao pode ser negativo")
self._preco = valor
p = Produto(10)
p.preco = 15 # dispara o setter (valida)
print(p.preco) # 15 <- dispara o getterDo lado de fora, p.preco parece um atributo comum, mas por dentro passa pela validação. Você começa com um atributo simples e, no dia em que precisar validar, vira propriedade sem quebrar o código que já usa a classe. Esse é o jeito pythônico: nada de escrever getter e setter para tudo desde o começo, como em outras linguagens.
Herança: reaproveitar com super()
Uma conta corrente é uma conta que aceita usar limite. Em vez de reescrever depósito e saque, você herda de Conta e acrescenta o que muda. O super() chama o construtor do pai para reaproveitar a montagem que ele já faz:
class Conta:
def __init__(self, dono, saldo=0):
self.dono = dono
self.saldo = saldo
class ContaCorrente(Conta):
def __init__(self, dono, saldo=0, limite=0):
super().__init__(dono, saldo) # roda o __init__ do pai
self.limite = limite
def sacar(self, valor):
if valor > self.saldo + self.limite:
print("Excede o limite")
else:
self.saldo -= valor
cc = ContaCorrente("Ana", 100, limite=200)
cc.sacar(250)
print(cc.saldo) # -150A ContaCorrente ganha de graça o que a Conta tem, aproveita o construtor com super().__init__(...) e só acrescenta o limite e a nova regra de saque. Use herança quando a frase é um faz sentido (conta corrente é uma conta). Se a relação for tem um, prefira composição, o que veremos adiante.
Polimorfismo e duck typing
Polimorfismo é poder chamar o mesmo método em objetos de tipos diferentes e cada um responder do seu jeito. Em Python isso vem do duck typing, resumido na frase: se anda como pato e faz quack como pato, trate como pato. O Python não checa a classe do objeto antes de chamar o método. Ele só tenta chamar. Se o método existe, funciona:
class Pato:
def falar(self):
return "Quack"
class Cachorro:
def falar(self):
return "Au"
def apresentar(animal):
print(animal.falar()) # nao pergunta o tipo, so chama
for a in [Pato(), Cachorro()]:
apresentar(a) # Quack / AuA função apresentar não sabe nem se importa se recebeu um pato ou um cachorro. Ela confia que o objeto tem um método falar. Adicionar um Gato depois não exige mexer na função: basta a classe nova ter o método. É isso que deixa código Python flexível, sem precisar de uma classe pai comum só para agrupar tipos parecidos.
Abstração com ABC: contratos que obrigam
Às vezes você quer garantir que toda classe de uma família implemente certo método. Uma classe base abstrata (ABC, de Abstract Base Class) faz esse contrato. Ela não pode ser instanciada e marca métodos como obrigatórios com @abstractmethod:
from abc import ABC, abstractmethod
class Forma(ABC):
@abstractmethod
def area(self):
...
class Quadrado(Forma):
def __init__(self, lado):
self.lado = lado
def area(self):
return self.lado ** 2
# Forma() # TypeError: nao da para instanciar classe abstrata
q = Quadrado(4)
print(q.area()) # 16Se você criar uma classe que herda de Forma e esquecer de implementar area, o Python reclama na hora de instanciar, com TypeError. É útil em projeto grande, quando várias pessoas criam formas e você quer garantir que todas tenham area. Para script pequeno, o duck typing sozinho costuma bastar: use ABC quando o contrato precisa ser cobrado.
Métodos dunder (mágicos)
Métodos com dois sublinhados na frente e atrás, como __init__, são chamados de dunder (de double underscore) ou mágicos. O Python os aciona em situações especiais: print(obj) chama __str__, len(obj) chama __len__, obj + outro chama __add__. Definir esses métodos deixa seus objetos se comportarem como os tipos nativos.
| Método | Quando dispara | Serve para |
|---|---|---|
| __init__ | ao criar o objeto | montar o estado inicial |
| __str__ | print(obj), str(obj) | texto amigável para o usuário |
| __repr__ | repr(obj), console, depurador | representação técnica para quem programa |
| __eq__ | obj == outro | comparar por valor, não por identidade |
| __len__ | len(obj) | devolver um tamanho |
| __add__ | obj + outro | somar dois objetos |
class Dinheiro:
def __init__(self, centavos):
self.centavos = centavos
def __repr__(self):
return f"Dinheiro({self.centavos})"
def __str__(self):
return f"R$ {self.centavos / 100:.2f}"
def __eq__(self, outro):
return self.centavos == outro.centavos
def __add__(self, outro):
return Dinheiro(self.centavos + outro.centavos)
a = Dinheiro(1050)
b = Dinheiro(200)
print(a + b) # R$ 12.50
print(a == Dinheiro(1050)) # TrueA diferença entre __str__ e __repr__ confunde muita gente. O __str__ é o texto bonito que print mostra. O __repr__ é a versão técnica que aparece no console e no depurador, e o ideal é que mostre algo capaz de recriar o objeto. Se você definir apenas __repr__, o Python o usa também no lugar do __str__. Por isso, quando estiver com pressa, comece pelo __repr__.
Método de instância, @classmethod e @staticmethod
Nem todo método precisa de um objeto. Existem três tipos, e a diferença está no primeiro parâmetro. O método de instância recebe self e mexe nos dados do objeto. O @classmethod recebe cls (a própria classe) e serve muito como construtor alternativo. O @staticmethod não recebe nada automático: é uma função comum que mora dentro da classe porque tem a ver com o tema.
| Tipo | Primeiro parâmetro | Uso típico |
|---|---|---|
| Método de instância | self | comportamento que usa os dados do objeto |
| @classmethod | cls | construtor alternativo, cria instâncias |
| @staticmethod | nenhum | função utilitária ligada ao tema da classe |
class Data:
def __init__(self, dia, mes, ano):
self.dia = dia
self.mes = mes
self.ano = ano
@classmethod
def de_string(cls, texto):
dia, mes, ano = map(int, texto.split("/"))
return cls(dia, mes, ano) # cls e a classe Data
@staticmethod
def e_bissexto(ano):
return ano % 4 == 0 and (ano % 100 != 0 or ano % 400 == 0)
d = Data.de_string("25/12/2026")
print(d.dia) # 25
print(Data.e_bissexto(2024)) # TrueO de_string é um segundo jeito de criar uma Data, a partir de texto, e por isso é @classmethod: precisa da classe para chamar cls(...). O e_bissexto só faz uma conta com um ano e não toca em nenhum objeto, então é @staticmethod.
dataclasses: menos código repetido
Muita classe existe só para carregar dados: um ponto, um produto, uma linha de planilha. Escrever __init__, __repr__ e __eq__ na mão para cada uma cansa. O decorador @dataclass, do módulo dataclasses, gera esses três de graça a partir dos campos que você declara:
from dataclasses import dataclass
@dataclass
class Ponto:
x: int
y: int
p = Ponto(2, 3)
print(p) # Ponto(x=2, y=3) <- __repr__ pronto
print(p == Ponto(2, 3)) # True <- __eq__ prontoSem o decorador, aquele print(p) mostraria algo como <__main__.Ponto object at 0x...> e a comparação daria False mesmo com os mesmos valores. Com @dataclass, você ganha impressão legível e comparação por valor sem escrever nada. Use quando a classe é basicamente um pacote de dados.
Herança múltipla e o MRO
Python permite herdar de mais de uma classe ao mesmo tempo. Isso levanta uma pergunta: se duas classes pai têm o mesmo método, qual vale? A resposta é o MRO (Method Resolution Order, ordem de resolução de métodos), a ordem em que o Python procura o método subindo pela hierarquia:
class A:
def cumprimentar(self):
return "A"
class B(A):
def cumprimentar(self):
return "B"
class C(A):
def cumprimentar(self):
return "C"
class D(B, C):
pass
d = D()
print(d.cumprimentar()) # B <- B vem antes de C no MRO
print([cls.__name__ for cls in D.__mro__])
# ['D', 'B', 'C', 'A', 'object']A classe D herda de B e C, que herdam de A. Pelo MRO, o Python olha primeiro D, depois B, depois C, depois A e, no fim, object (toda classe herda de object). Por isso d.cumprimentar() devolve B. Você consulta essa ordem com D.__mro__. Herança múltipla é poderosa, mas cria essas dúvidas: use com parcimônia e, na dúvida, prefira composição.
Composição x herança: prefira composição
Herança não é a única forma de reaproveitar. Na composição, um objeto guarda outro objeto por dentro em vez de herdar dele. A regra prática, repetida por quem programa há tempo, é: prefira composição a herança. Um carro tem um motor, ele não é um motor, então composição encaixa melhor:
class Motor:
def ligar(self):
return "vrum"
class Carro:
def __init__(self):
self.motor = Motor() # o carro TEM um motor
def dar_partida(self):
return self.motor.ligar()
c = Carro()
print(c.dar_partida()) # vrumA vantagem é o baixo acoplamento. Trocar o Motor por um MotorEletrico não obriga a mexer na hierarquia de classes, só a montar o carro com outra peça. Herança amarra a filha ao pai para sempre; composição deixa você trocar as partes. Use herança quando a relação é um é clara e estável; no resto, componha.
Quando não usar POO
Ao contrário de linguagens onde tudo precisa morar dentro de uma classe, o Python não obriga. Um script que lê um arquivo, filtra linhas e imprime um total fica mais claro com funções simples do que embrulhado numa classe que existe só para dar nome a nada. Sinais de que a classe está sobrando: ela tem só um método além do __init__, ou guarda dados que nunca ganham comportamento. Nesses casos, uma função ou um dicionário resolve.
A POO ganha valor quando você percebe grupos de dados que sempre andam juntos e ganham comportamento próprio: um usuário com seus dados e suas ações, um pedido que sabe calcular o próprio total. Se você está aprendendo do zero, vale firmar antes a base de lógica de programação e o conceito de o que é um algoritmo antes de se preocupar com classes.
Erros comuns de quem está começando
- Esquecer o
selfnos métodos ou ao ler um atributo (usarsaldoem vez deself.saldo). - Usar lista ou dicionário como atributo de classe sem querer, e ver o dado vazar entre objetos. Dados que variam por objeto vão no
__init__. - Achar que
__nomeé privado de verdade. É só name mangling: vira_Classe__nomee continua acessível. - Criar classe para tudo. Dado simples sem comportamento pode continuar sendo variável ou dicionário.
- Abusar de herança e montar cadeias longas de classes que herdam de classes. Muitas vezes composição resolve com menos amarração.
- Deixar todos os dados públicos e mexer neles de fora, o que desfaz o encapsulamento que a classe tentou criar.
O curso de Python intermediário cobre classes, herança, polimorfismo e dataclasses com playground no navegador, exercícios corrigidos e certificado, sem custo. Se você ainda está no começo, comece pelo curso de Python do zero; para ir além, siga no curso de Python avançado. Quem nunca programou pode começar pelo curso de lógica de programação.
Limitações deste guia
Este material é educativo e cobre a base da orientação a objetos em Python até um nível intermediário. Ele não substitui a documentação oficial da linguagem, que traz detalhes de casos mais raros, como métodos especiais menos usados, descritores, metaclasses e regras finas de MRO em hierarquias grandes. Os exemplos foram escritos para rodar em Python 3 e são propositalmente curtos: em código de produção você somaria tratamento de erros, testes e anotações de tipo. Ao aplicar num projeto real, confira sempre o comportamento na versão de Python que você usa.
Fontes oficiais
- Documentação oficial do Python: Classes: referência da sintaxe de classes, herança, escopo e name mangling.
- Documentação oficial do Python: dataclasses: o decorador
@dataclasse o que ele gera automaticamente. - Documentação oficial do Python: abc: classes base abstratas e
@abstractmethod. - Python.org: site oficial da linguagem, com o tutorial de introdução completo.
Conclusão
Orientação a objetos é agrupar dados e comportamento em classes e criar objetos a partir delas. Encapsule para proteger os dados, lembrando que em Python isso é convenção e não cadeado. Use herança e super() só quando existe relação de é um, e prefira composição no resto. Deixe o polimorfismo (duck typing) simplificar o código que trata muitos tipos, cobre contratos com ABC quando precisar, e economize código repetido com @dataclass. A melhor forma de fixar é escrever suas próprias classes e testar cada conceito. Para praticar com playground, exercícios e certificado gratuitos, use o curso de Python intermediário. Depois, aprofunde em decorators em Python e em estruturas de dados e algoritmos, explore o guia de como aprender Python do zero e as estruturas de dados avançadas. Ferramentas como o formatador de JSON e o gerador de hash ajudam no dia a dia de quem programa. Veja também todos os cursos gratuitos, a categoria Tecnologia e como validamos os conteúdos.