Criação vs. inicialização: o que realmente acontece ao instanciar
Ao escrever obj = MinhaClasse(...), duas etapas diferentes entram em jogo:
- Criação do objeto: ocorre em
__new__(raramente você precisa sobrescrever). É quando a instância é alocada. - Inicialização do objeto: ocorre em
__init__. É quando você configura o estado inicial (atributos) e valida regras.
Na prática, para a maioria das classes do dia a dia, você foca em __init__. Um ponto importante: __init__ não cria o objeto; ele recebe uma instância já criada e deve deixá-la em um estado válido.
O papel do __init__: garantir um estado inicial válido
Um construtor bem escrito faz duas coisas com consistência:
- Define todos os atributos necessários (evita atributos “aparecendo” depois).
- Valida invariantes (regras que sempre devem ser verdadeiras para a instância ser considerada válida).
Exemplo de invariantes comuns: quantidade não pode ser negativa, e-mail deve conter @, saldo não pode ser menor que zero, data final deve ser após data inicial.
Exemplo: classe com validações de invariantes
class Produto:
def __init__(self, nome: str, preco: float, estoque: int = 0):
if not isinstance(nome, str) or not nome.strip():
raise ValueError("nome deve ser uma string não vazia")
if preco <= 0:
raise ValueError("preco deve ser > 0")
if estoque < 0:
raise ValueError("estoque não pode ser negativo")
self.nome = nome.strip()
self.preco = float(preco)
self.estoque = int(estoque)Repare que: (1) os atributos são definidos apenas após validações; (2) há conversões controladas (float, int) para padronizar o tipo armazenado.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Parâmetros posicionais e nomeados no __init__
Ao criar instâncias, você pode passar argumentos por posição ou por nome (keyword). Ambos funcionam, mas têm usos diferentes:
- Posicionais: mais curtos, porém mais fáceis de errar quando há muitos parâmetros.
- Nomeados: mais legíveis e robustos contra mudanças na ordem dos parâmetros.
Exemplo prático de instância com diferentes formas de chamada
p1 = Produto("Caderno", 19.9) # estoque usa o padrão 0
p2 = Produto("Caneta", 3.5, 100) # tudo por posição
p3 = Produto(nome="Borracha", preco=2.0) # nomeados
p4 = Produto(preco=10.0, nome="Agenda", estoque=5) # ordem livre com nomeadosArmadilha: misturar posicionais e nomeados de forma inválida
Em Python, argumentos posicionais devem vir antes dos nomeados. Isto é inválido:
# Produto("Caderno", preco=19.9, 10) # SyntaxError: positional argument follows keyword argumentValores padrão: quando usar e como escolher
Valores padrão tornam a criação de instâncias mais conveniente, mas precisam ser escolhidos com cuidado para não mascarar erros.
Boas práticas para valores padrão
- Use padrões que representem um estado válido (ex.:
estoque=0pode ser válido;preco=0geralmente não). - Evite padrões “mágicos” que escondem ausência de informação (ex.:
data=""ou-1), prefiraNonequando o valor é opcional. - Valide mesmo com padrão: o fato de ter padrão não elimina a necessidade de invariantes.
Padrão com None para campos opcionais
class Usuario:
def __init__(self, nome: str, email: str | None = None):
if not nome or not nome.strip():
raise ValueError("nome é obrigatório")
if email is not None and "@" not in email:
raise ValueError("email inválido")
self.nome = nome.strip()
self.email = emailAqui, email pode ser omitido, mas se for fornecido, precisa ser válido.
Validações de invariantes: estratégias e ordem correta
Uma regra útil: valide antes de atribuir. Isso evita que a instância fique parcialmente configurada se uma validação falhar no meio.
Passo a passo recomendado
- Receba parâmetros (posicionais/nomeados) e valores padrão.
- Valide tipos/formatos e regras de negócio (invariantes).
- Normalize dados (ex.:
strip, conversões, padronização de caixa). - Atribua aos atributos da instância.
Exemplo: normalização + validação
class Cliente:
def __init__(self, nome: str, cpf: str):
nome_limpo = (nome or "").strip()
cpf_limpo = (cpf or "").strip()
if not nome_limpo:
raise ValueError("nome é obrigatório")
if len(cpf_limpo) != 11 or not cpf_limpo.isdigit():
raise ValueError("cpf deve ter 11 dígitos numéricos")
self.nome = nome_limpo
self.cpf = cpf_limpoArmadilha comum: usar mutáveis como valor padrão
Em Python, valores padrão são avaliados uma única vez no momento em que a função é definida, não a cada chamada. Isso causa compartilhamento acidental quando o padrão é mutável (lista, dicionário, conjunto).
Exemplo do problema
class Turma:
def __init__(self, nome: str, alunos: list[str] = []):
self.nome = nome
self.alunos = alunos
def adicionar(self, aluno: str):
self.alunos.append(aluno)
# Problema: a lista padrão é a mesma para todas as instâncias
t1 = Turma("A")
t2 = Turma("B")
t1.adicionar("Ana")
print(t2.alunos) # ['Ana'] (inesperado)Correção robusta com None
class Turma:
def __init__(self, nome: str, alunos: list[str] | None = None):
self.nome = nome
self.alunos = list(alunos) if alunos is not None else []
def adicionar(self, aluno: str):
self.alunos.append(aluno)Além de evitar o padrão mutável, list(alunos) cria uma cópia, protegendo a instância contra alterações externas na lista original.
Boas práticas para evitar estados inválidos
- Não deixe atributos “opcionais” sem necessidade: se um atributo é obrigatório para o objeto funcionar, exija no
__init__. - Evite inicialização em duas fases (criar e depois “configurar”): prefira receber tudo no construtor ou use métodos de fábrica bem definidos.
- Falhe cedo: valide e lance exceções no
__init__quando algo estiver errado. - Não exponha estruturas internas diretamente quando isso permitir quebrar invariantes (ex.: retornar a lista interna sem cópia).
- Padronize tipos armazenados (ex.: sempre
floatpara preço, sempreintpara quantidade).
Seção prática: refatorando classes mal inicializadas
Caso 1: atributos faltando e validação ausente
Versão frágil (estado inválido possível):
class Pedido:
def __init__(self, cliente, itens=None):
self.cliente = cliente
self.itens = itens
self.total = 0
def calcular_total(self):
self.total = sum(item["preco"] for item in self.itens)Problemas típicos:
clientepode ser vazio/nulo sem erro.itenspode serNone, ecalcular_totalquebra.- Não há validação do formato dos itens.
Refatoração passo a passo:
- Definir um padrão seguro para
itens(lista vazia). - Validar
cliente. - Validar itens e normalizar estrutura.
- Evitar depender de método separado para ter um estado coerente (total pode ser calculado na inicialização ou sob demanda).
Versão mais robusta:
class Pedido:
def __init__(self, cliente: str, itens: list[dict] | None = None):
cliente_limpo = (cliente or "").strip()
if not cliente_limpo:
raise ValueError("cliente é obrigatório")
itens_lista = list(itens) if itens is not None else []
for i, item in enumerate(itens_lista):
if not isinstance(item, dict):
raise ValueError(f"item {i} deve ser dict")
if "preco" not in item:
raise ValueError(f"item {i} sem chave 'preco'")
if item["preco"] < 0:
raise ValueError(f"item {i} com preco negativo")
self.cliente = cliente_limpo
self.itens = itens_lista
@property
def total(self) -> float:
return float(sum(item["preco"] for item in self.itens))Aqui, total vira uma propriedade calculada, reduzindo risco de ficar desatualizado.
Caso 2: padrão mutável e falta de cópia defensiva
Versão frágil:
class Config:
def __init__(self, opcoes={}):
self.opcoes = opcoesVersão robusta:
class Config:
def __init__(self, opcoes: dict | None = None):
self.opcoes = dict(opcoes) if opcoes is not None else {}Caso 3: inicialização parcial e atributos criados depois
Versão frágil:
class Conexao:
def __init__(self, host: str):
self.host = host
def autenticar(self, token: str):
self.token = tokenProblema: a instância pode existir sem token, e outros métodos podem assumir que ele existe. Se o token é obrigatório para o objeto ser usado, ele deve entrar no __init__ (ou o objeto deve representar explicitamente um estado “não autenticado”).
Versão robusta (token obrigatório):
class Conexao:
def __init__(self, host: str, token: str):
host_limpo = (host or "").strip()
token_limpo = (token or "").strip()
if not host_limpo:
raise ValueError("host é obrigatório")
if not token_limpo:
raise ValueError("token é obrigatório")
self.host = host_limpo
self.token = token_limpoAlternativa (token opcional, mas estado explícito):
class Conexao:
def __init__(self, host: str, token: str | None = None):
host_limpo = (host or "").strip()
if not host_limpo:
raise ValueError("host é obrigatório")
self.host = host_limpo
self.token = token.strip() if token is not None else None
def esta_autenticada(self) -> bool:
return self.token is not NoneExercícios: criação de instâncias em diferentes cenários
Exercício 1: instanciando com posicionais e nomeados
Dada a classe:
class Assinatura:
def __init__(self, plano: str, preco: float, renovacao_automatica: bool = True):
if plano not in {"basic", "pro", "enterprise"}:
raise ValueError("plano inválido")
if preco <= 0:
raise ValueError("preco inválido")
self.plano = plano
self.preco = float(preco)
self.renovacao_automatica = bool(renovacao_automatica)- Crie uma instância
basiccom preço29.9usando apenas posicionais. - Crie uma instância
procom preço59.9desativando renovação automática usando argumentos nomeados. - Tente criar uma instância com
plano="gold"e observe a exceção esperada.
Exercício 2: corrigindo padrão mutável
Refatore a classe abaixo para evitar compartilhamento de lista entre instâncias e para copiar a lista recebida:
class Playlist:
def __init__(self, nome: str, musicas: list[str] = []):
self.nome = nome
self.musicas = musicasDepois, crie duas playlists sem passar músicas e adicione uma música em apenas uma delas. Verifique se a outra permanece vazia.
Exercício 3: invariantes e normalização
Implemente uma classe ReservaHotel com:
hospede(obrigatório, string não vazia, deve ser armazenado comstrip())noites(obrigatório, inteiro > 0)cupom(opcional,Nonepor padrão; se fornecido, deve ser string não vazia apósstrip())
Crie instâncias cobrindo estes cenários:
- Sem cupom.
- Com cupom válido.
- Com
noites=0(deve falhar). - Com
hospedevazio (deve falhar).
Exercício 4: evitando inicialização parcial
Você recebeu uma classe que cria atributos depois:
class Relatorio:
def __init__(self, titulo: str):
self.titulo = titulo
def definir_dados(self, dados: list[dict]):
self.dados = dados- Refatore para que
dadosseja obrigatório no__init__e validado (lista). - Alternativamente, refatore para que
dadosseja opcional, mas o objeto exponha um métodotem_dados()e nunca quebre ao acessardados(por exemplo, usando lista vazia).