Classes, objetos, herança, encapsulamento, polimorfismo e composição — do zero ao exemplo real.
POO é um paradigma onde você organiza o código em objetos, que representam coisas do mundo real.
🚗 Carro
Atributos: cor, modelo
Ações: acelerar, frear
👤 Usuário
Atributos: nome, email
Ações: fazer login
🏦 Conta
Atributos: saldo, titular
Ações: depositar, sacar
🐾 Animal
Atributos: nome, espécie
Ações: falar, mover
Classe — um molde para criar objetos:
class Pessoa:
pass # classe vazia por enquanto
Objeto — uma instância criada a partir da classe:
p1 = Pessoa() # cria um objeto do tipo Pessoa
p2 = Pessoa() # outro objeto, independente
Atributos e Métodos juntos:
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome # atributo
self.idade = idade # atributo
def apresentar(self): # método
print(f"Olá, meu nome é {self.nome}")
p1 = Pessoa("Igor", 25)
p1.apresentar() # Olá, meu nome é Igor
print(p1.nome) # Igor
print(p1.idade) # 25
__init__ é o construtor — executado automaticamente ao criar um objeto:
class Carro:
def __init__(self, modelo, cor):
self.modelo = modelo
self.cor = cor
meu_carro = Carro("Fusca", "azul")
# __init__ roda aqui automaticamente ↑
self representa o próprio objeto — permite acessar atributos e métodos da instância:
class Carro:
def __init__(self, modelo):
self.modelo = modelo # "self" = este objeto
def descrever(self):
print(self.modelo) # acessa atributo da instância
self é apenas uma convenção de nome — poderia ser qualquer nome, mas sempre deve ser o primeiro parâmetro dos métodos de instância.Serve para proteger e controlar o acesso aos dados de um objeto.
self.nome
🟢 Público — acessível de qualquer lugar
self._nome
🟡 Protegido — convenção: não usar fora da classe
self.__nome
🔴 Privado — name mangling, não acessível diretamente
class Conta:
def __init__(self, saldo):
self.__saldo = saldo # privado
def ver_saldo(self):
return self.__saldo # acesso controlado via método
conta = Conta(1000)
print(conta.ver_saldo()) # ✅ 1000
# print(conta.__saldo) # ❌ AttributeError
__nome vira _Classe__nome internamente (name mangling) — é uma proteção por convenção, não por segurança absoluta.Controlam o acesso e modificação de atributos protegidos.
Forma manual (getters/setters):
class Pessoa:
def __init__(self, nome):
self._nome = nome
def get_nome(self): # getter
return self._nome
def set_nome(self, nome): # setter
self._nome = nome
✨ Forma moderna com @property:
class Pessoa:
def __init__(self, nome):
self._nome = nome
@property
def nome(self): # getter
return self._nome
@nome.setter
def nome(self, valor): # setter
if len(valor) > 0:
self._nome = valor
p = Pessoa("Igor")
print(p.nome) # Igor — chama getter automaticamente
p.nome = "Carlos" # chama setter automaticamente
@property você acessa como atributo (p.nome), mas por baixo dos panos roda o método. É o jeito mais pythônico de criar getters/setters.Permite que uma classe herde atributos e métodos de outra — reutilização de código.
class Animal: # classe pai (base)
def __init__(self, nome):
self.nome = nome
def falar(self):
print("Som genérico")
class Cachorro(Animal): # herda de Animal
def falar(self): # sobrescreve o método
print(f"{self.nome} diz: Au au!")
class Gato(Animal):
def falar(self):
print(f"{self.nome} diz: Miau!")
rex = Cachorro("Rex")
bilu = Gato("Bilu")
rex.falar() # Rex diz: Au au!
bilu.falar() # Bilu diz: Miau!
Usando super() para chamar o constructor do pai:
class ContaCorrente(Conta):
def __init__(self, titular, saldo, limite):
super().__init__(titular, saldo) # chama __init__ do pai
self.limite = limite
Mesma chamada de método — comportamentos diferentes dependendo do objeto.
animais = [Cachorro("Rex"), Gato("Bilu"), Animal("Peixe")]
for animal in animais:
animal.falar()
# Rex diz: Au au!
# Bilu diz: Miau!
# Som genérico
Oculta a complexidade e expõe apenas o necessário. Em Python usamos classes abstratas com o módulo abc.
from abc import ABC, abstractmethod
class Forma(ABC): # classe abstrata
@abstractmethod
def area(self): # método abstrato
pass
class Circulo(Forma):
def __init__(self, raio):
self.raio = raio
def area(self): # obrigado a implementar
return 3.14 * self.raio ** 2
class Retangulo(Forma):
def __init__(self, l, a):
self.l, self.a = l, a
def area(self):
return self.l * self.a
Forma() vai lançar TypeError. As subclasses são obrigadas a implementar todos os métodos abstratos.Métodos com duplo underscore — chamados automaticamente pelo Python em situações específicas.
class Pessoa:
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def __str__(self): # para print()
return f"{self.nome} ({self.idade} anos)"
def __repr__(self): # representação técnica
return f"Pessoa(nome='{self.nome}', idade={self.idade})"
def __len__(self): # para len()
return len(self.nome)
p = Pessoa("Igor", 25)
print(p) # Igor (25 anos)
print(repr(p)) # Pessoa(nome='Igor', idade=25)
print(len(p)) # 4
Principais dunder methods:
| Método | Quando é chamado |
|---|---|
| __init__ | Ao criar o objeto (Classe()) |
| __str__ | Ao usar print(obj) ou str(obj) |
| __repr__ | Representação técnica do objeto |
| __len__ | Ao usar len(obj) |
| __add__ | Ao usar obj1 + obj2 |
| __eq__ | Ao usar obj1 == obj2 |
| __lt__ / __gt__ | Comparações < e > |
| __del__ | Ao deletar o objeto |
class Exemplo:
contador = 0 # atributo de classe
def metodo_instancia(self): # acessa self
print("instância")
@classmethod
def metodo_classe(cls): # acessa a classe (cls)
cls.contador += 1
print(f"Total criados: {cls.contador}")
@staticmethod
def metodo_estatico(): # não acessa self nem cls
print("utilitário independente")
def met(self)
Método de instância — acessa self. O mais comum.
@classmethod
Método de classe — acessa cls. Útil para factories.
@staticmethod
Método estático — não acessa instância nem classe. Funções utilitárias.
Em vez de herdar, você usa objetos dentro de outros objetos. Muitas vezes é preferível à herança.
# herança (Carro É UM veículo)
class Carro(Veiculo): pass
# composição (Carro TEM UM motor)
class Motor:
def ligar(self):
print("Motor ligado")
class Carro:
def __init__(self):
self.motor = Motor() # composição
def ligar(self):
self.motor.ligar() # delega ao motor
meu_carro = Carro()
meu_carro.ligar() # Motor ligado
class Conta:
def __init__(self, titular, saldo=0):
self.titular = titular
self.__saldo = saldo # privado
def depositar(self, valor):
self.__saldo += valor
def sacar(self, valor):
if valor <= self.__saldo:
self.__saldo -= valor
else:
print("Saldo insuficiente")
@property
def saldo(self):
return self.__saldo
def __str__(self):
return f"Conta({self.titular}) — R$ {self.__saldo:.2f}"
class ContaCorrente(Conta): # herança
def sacar(self, valor):
print("Sacando da conta corrente…")
super().sacar(valor) # chama método do pai
# uso
conta = ContaCorrente("Igor", 100)
conta.depositar(50)
conta.sacar(30)
print(conta) # Conta(Igor) — R$ 120.00
print(conta.saldo) # 120
✔ Boas práticas:
Pessoa, Produto, ContaCorrente__atributo e use @property❌ Erros comuns:
# ❌ Esquecer o self no método
def metodo(): # falta self
pass
# ❌ Atributo de classe ao invés de instância
class Pessoa:
nome = "Igor" # compartilhado entre todas as instâncias
# ✅ Correto: atributo de instância no __init__
class Pessoa:
def __init__(self, nome):
self.nome = nome # cada objeto tem o seu
# ❌ Acessar atributo privado diretamente
obj.__saldo # AttributeError
# ✅ Use o @property ou método público
__init__ + self → Métodos → Encapsulamento → Herança → Polimorfismo → Composição