Construtores e inicialização de objetos em POO Python: __init__ e valores padrão

Capítulo 3

Tempo estimado de leitura: 9 minutos

+ Exercício

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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 nomeados

Armadilha: 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 argument

Valores 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=0 pode ser válido; preco=0 geralmente não).
  • Evite padrões “mágicos” que escondem ausência de informação (ex.: data="" ou -1), prefira None quando 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 = email

Aqui, 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

  1. Receba parâmetros (posicionais/nomeados) e valores padrão.
  2. Valide tipos/formatos e regras de negócio (invariantes).
  3. Normalize dados (ex.: strip, conversões, padronização de caixa).
  4. 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_limpo

Armadilha 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 float para preço, sempre int para 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:

  • cliente pode ser vazio/nulo sem erro.
  • itens pode ser None, e calcular_total quebra.
  • Não há validação do formato dos itens.

Refatoração passo a passo:

  1. Definir um padrão seguro para itens (lista vazia).
  2. Validar cliente.
  3. Validar itens e normalizar estrutura.
  4. 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 = opcoes

Versã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 = token

Problema: 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_limpo

Alternativa (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 None

Exercí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 basic com preço 29.9 usando apenas posicionais.
  • Crie uma instância pro com preço 59.9 desativando 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 = musicas

Depois, 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 com strip())
  • noites (obrigatório, inteiro > 0)
  • cupom (opcional, None por padrão; se fornecido, deve ser string não vazia após strip())

Crie instâncias cobrindo estes cenários:

  • Sem cupom.
  • Com cupom válido.
  • Com noites=0 (deve falhar).
  • Com hospede vazio (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 dados seja obrigatório no __init__ e validado (lista).
  • Alternativamente, refatore para que dados seja opcional, mas o objeto exponha um método tem_dados() e nunca quebre ao acessar dados (por exemplo, usando lista vazia).

Agora responda o exercício sobre o conteúdo:

Ao definir um parâmetro opcional no __init__ que recebe uma coleção (ex.: lista de alunos), qual abordagem evita o compartilhamento acidental de dados entre instâncias e ainda protege contra mudanças externas na lista passada?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Valores padrão mutáveis (como []) são avaliados uma vez e podem ser compartilhados entre instâncias. Usar None e criar uma nova lista (com list(alunos) quando fornecida) evita o compartilhamento e faz cópia defensiva.

Próximo capitúlo

Encapsulamento em Python Orientado a Objetos: convenções, validação e invariantes

Arrow Right Icon
Capa do Ebook gratuito Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas
21%

Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas

Novo curso

14 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.