Classes em Python: atributos, métodos e o parâmetro self

Capítulo 2

Tempo estimado de leitura: 9 minutos

+ Exercício

Declarando uma classe: estrutura mínima

Uma classe define um “molde” com dados (atributos) e comportamentos (métodos). Em Python, você declara uma classe com class e define métodos como funções dentro dela.

class ContaBancaria:
    pass

Na prática, quase sempre você vai incluir um construtor (__init__) para inicializar o estado do objeto.

Passo a passo: criando uma classe com estado inicial

Vamos criar uma conta com titular e saldo inicial. Repare no parâmetro self e no uso de self.atributo para guardar dados no objeto.

class ContaBancaria:
    def __init__(self, titular, saldo_inicial=0):
        self.titular = titular
        self.saldo = saldo_inicial

c1 = ContaBancaria("Ana", 100)
print(c1.titular)  # Ana
print(c1.saldo)    # 100

O que é o parâmetro self (e por que ele é obrigatório)

self é uma referência ao próprio objeto que está executando o método. Quando você chama c1.saldo ou c1 executa um método, é esse objeto que deve ser acessado dentro do método. Por convenção, o primeiro parâmetro de métodos de instância se chama self.

Ao chamar um método, Python passa o objeto automaticamente:

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

  • c1.depositar(50) equivale a ContaBancaria.depositar(c1, 50)
class ContaBancaria:
    def __init__(self, titular, saldo_inicial=0):
        self.titular = titular
        self.saldo = saldo_inicial

    def depositar(self, valor):
        self.saldo += valor

c1 = ContaBancaria("Ana", 100)
c1.depositar(50)
print(c1.saldo)  # 150

Atributos de instância vs atributos de classe

Atributos de instância

São guardados em cada objeto individualmente. Você normalmente os cria no __init__ usando self. Cada instância tem seus próprios valores.

class Usuario:
    def __init__(self, nome):
        self.nome = nome

u1 = Usuario("Ana")
u2 = Usuario("Bruno")
print(u1.nome)  # Ana
print(u2.nome)  # Bruno

Atributos de classe

São definidos diretamente no corpo da classe e pertencem à classe como um todo. Eles são compartilhados por todas as instâncias (a menos que você sobrescreva em uma instância específica).

class Usuario:
    especie = "Humano"  # atributo de classe

    def __init__(self, nome):
        self.nome = nome  # atributo de instância

u1 = Usuario("Ana")
u2 = Usuario("Bruno")
print(u1.especie)  # Humano
print(u2.especie)  # Humano

Cuidado: atributo de classe mutável (armadilha comum)

Se o atributo de classe for mutável (lista, dicionário, conjunto), todas as instâncias podem acabar compartilhando o mesmo objeto, causando efeitos colaterais difíceis de perceber.

class Carrinho:
    itens = []  # armadilha: lista compartilhada

    def adicionar(self, item):
        self.itens.append(item)

c1 = Carrinho()
c2 = Carrinho()

c1.adicionar("maçã")
print(c2.itens)  # ['maçã'] (surpresa!)

Para que cada instância tenha sua própria lista, crie o atributo no __init__:

class Carrinho:
    def __init__(self):
        self.itens = []  # lista por instância

    def adicionar(self, item):
        self.itens.append(item)

Métodos que operam no estado interno: consultas vs comandos

Um padrão útil é separar métodos em dois tipos:

  • Consultas (queries): retornam informações e não alteram o estado do objeto.
  • Comandos (commands): alteram o estado do objeto (e geralmente retornam None).

Exemplo prático: Conta com consultas e comandos

class ContaBancaria:
    def __init__(self, titular, saldo_inicial=0):
        self.titular = titular
        self.saldo = saldo_inicial

    # Consulta: não altera estado
    def saldo_atual(self):
        return self.saldo

    # Comando: altera estado
    def depositar(self, valor):
        self._validar_valor_positivo(valor)
        self.saldo += valor

    # Comando: altera estado
    def sacar(self, valor):
        self._validar_valor_positivo(valor)
        if valor > self.saldo:
            raise ValueError("Saldo insuficiente")
        self.saldo -= valor

    # Método utilitário interno (detalhe de implementação)
    def _validar_valor_positivo(self, valor):
        if not isinstance(valor, (int, float)):
            raise TypeError("Valor deve ser numérico")
        if valor <= 0:
            raise ValueError("Valor deve ser positivo")

c = ContaBancaria("Ana", 100)
print(c.saldo_atual())  # 100
c.depositar(50)
print(c.saldo_atual())  # 150

Note o uso de _validar_valor_positivo como um método “utilitário” do objeto. O prefixo _ é uma convenção para indicar que é um detalhe interno e não faz parte da interface pública principal.

Evitando efeitos colaterais desnecessários

Efeito colateral é quando um método altera algo além do que o nome/contrato do método sugere. Alguns cuidados comuns:

  • Consultas não devem modificar atributos.
  • Evite alterar argumentos mutáveis recebidos (listas/dicts) sem deixar isso explícito.
  • Prefira retornar novos valores/estruturas quando a intenção for “calcular”, e não “atualizar”.

Exemplo: consulta que acidentalmente altera estado (evite)

class Termometro:
    def __init__(self):
        self.leituras = []

    def media(self):
        # ruim: muda o estado ao "consultar"
        self.leituras.append(0)
        return sum(self.leituras) / len(self.leituras)

Melhor: manter a consulta pura e deixar inserção de leitura em um comando separado.

class Termometro:
    def __init__(self):
        self.leituras = []

    def registrar(self, valor):
        self.leituras.append(valor)

    def media(self):
        if not self.leituras:
            return None
        return sum(self.leituras) / len(self.leituras)

Quando usar métodos de instância vs métodos utilitários

Métodos de instância

Use quando o comportamento depende do estado do objeto (self) ou quando faz sentido que a ação pertença àquele “tipo” de objeto.

  • conta.depositar(valor) altera self.saldo
  • carrinho.adicionar(item) altera self.itens

Métodos utilitários (funções auxiliares)

Se uma lógica não depende do estado do objeto, pode ser melhor como função fora da classe (ou como método estático). Isso reduz acoplamento e facilita testes.

Exemplo: validar CPF, formatar texto, calcular imposto a partir de um número, etc. Se não usa self, pergunte: “isso realmente pertence ao objeto?”

def normalizar_nome(nome):
    return " ".join(parte.capitalize() for parte in nome.split())

class Usuario:
    def __init__(self, nome):
        self.nome = normalizar_nome(nome)

u = Usuario("ana maria")
print(u.nome)  # Ana Maria

Se você quiser manter dentro da classe por organização, pode usar @staticmethod (não recebe self):

class Usuario:
    def __init__(self, nome):
        self.nome = self.normalizar_nome(nome)

    @staticmethod
    def normalizar_nome(nome):
        return " ".join(parte.capitalize() for parte in nome.split())

Passo a passo prático: implementando um objeto com operações típicas

Agora você vai construir uma classe com operações comuns: adicionar/remover itens, atualizar estado e validar entradas. O exemplo abaixo é um “estoque” simples.

1) Defina o estado (atributos de instância)

class Estoque:
    def __init__(self):
        self._itens = {}  # {nome: quantidade}

2) Crie comandos para alterar o estado (adicionar/remover/atualizar)

class Estoque:
    def __init__(self):
        self._itens = {}

    def adicionar(self, nome, quantidade=1):
        self._validar_nome(nome)
        self._validar_quantidade(quantidade)
        self._itens[nome] = self._itens.get(nome, 0) + quantidade

    def remover(self, nome, quantidade=1):
        self._validar_nome(nome)
        self._validar_quantidade(quantidade)
        atual = self._itens.get(nome, 0)
        if atual == 0:
            raise KeyError(f"Item '{nome}' não existe no estoque")
        if quantidade > atual:
            raise ValueError("Quantidade para remover excede o disponível")
        novo = atual - quantidade
        if novo == 0:
            del self._itens[nome]
        else:
            self._itens[nome] = novo

    def atualizar(self, nome, nova_quantidade):
        self._validar_nome(nome)
        self._validar_quantidade(nova_quantidade)
        if nova_quantidade == 0:
            self._itens.pop(nome, None)
        else:
            self._itens[nome] = nova_quantidade

    def _validar_nome(self, nome):
        if not isinstance(nome, str) or not nome.strip():
            raise ValueError("Nome do item deve ser uma string não vazia")

    def _validar_quantidade(self, quantidade):
        if not isinstance(quantidade, int):
            raise TypeError("Quantidade deve ser um inteiro")
        if quantidade < 0:
            raise ValueError("Quantidade não pode ser negativa")

3) Crie consultas para observar o impacto no objeto

Consultas ajudam a “enxergar” o estado sem alterá-lo.

class Estoque:
    def __init__(self):
        self._itens = {}

    # ... comandos e validações ...

    def quantidade(self, nome):
        self._validar_nome(nome)
        return self._itens.get(nome, 0)

    def listar(self):
        # retorna uma cópia para evitar que código externo altere o dicionário interno
        return dict(self._itens)

Repare no detalhe: listar retorna uma cópia. Isso evita que alguém faça estoque.listar()["arroz"] = 999 e altere o estado interno sem passar pelas validações.

4) Teste o comportamento e observe o estado mudando

e = Estoque()

e.adicionar("arroz", 2)
e.adicionar("feijão", 1)
print(e.listar())  # {'arroz': 2, 'feijão': 1}

print(e.quantidade("arroz"))  # 2

e.remover("arroz", 1)
print(e.listar())  # {'arroz': 1, 'feijão': 1}

e.atualizar("feijão", 0)
print(e.listar())  # {'arroz': 1}

Tarefas práticas (para implementar e observar o impacto no objeto)

Tarefa 1: Carrinho de compras com validações

  • Crie uma classe Carrinho com self.itens (lista de strings) ou um dicionário {produto: quantidade}.
  • Implemente comandos: adicionar(produto, qtd), remover(produto, qtd), limpar().
  • Implemente consultas: total_itens() e listar() (retornando cópia).
  • Valide: produto não vazio; qtd inteira e positiva; não permitir remover mais do que existe.

Tarefa 2: Controle de progresso (estado que evolui)

  • Crie ProgressoCurso com atributos de instância: total_aulas, assistidas.
  • Comandos: marcar_assistida(qtd=1) (não ultrapassar total), reiniciar().
  • Consultas: percentual() (0 a 100), faltam().
  • Valide entradas e garanta que consultas não alterem estado.

Tarefa 3: Conta com regras (comandos que podem falhar)

  • Expanda ContaBancaria para ter limite (cheque especial) e permitir saldo negativo até -limite.
  • Implemente sacar com validação: não permitir ultrapassar o limite.
  • Adicione uma consulta disponivel_para_saque() que retorne quanto ainda pode sacar.
  • Garanta que erros sejam comunicados com exceções apropriadas (ValueError, TypeError).

Tarefa 4: Identificando atributos de classe corretamente

  • Crie uma classe Produto com atributo de classe taxa_imposto (ex.: 0.1).
  • Cada instância tem nome e preco (atributos de instância).
  • Implemente uma consulta preco_com_imposto() que use Produto.taxa_imposto (ou self.__class__.taxa_imposto).
  • Teste alterando Produto.taxa_imposto e observe o impacto em todas as instâncias.
ElementoOnde ficaExemploImpacto
Atributo de instânciaNo objeto (self)self.saldoMuda por instância
Atributo de classeNa classeProduto.taxa_impostoCompartilhado entre instâncias
Método de instânciaRecebe selfdepositar(self, valor)Opera no estado do objeto
Função utilitáriaFora da classe (ou @staticmethod)normalizar_nome(nome)Não depende do estado

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

Ao implementar o método listar() em uma classe Estoque que guarda itens em um dicionário interno, qual prática ajuda a evitar que código externo altere o estado do objeto sem passar por validações?

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

Você errou! Tente novamente.

Ao retornar uma cópia, quem chama pode modificar o resultado sem afetar o dicionário interno do objeto. Assim, o estado só muda por meio dos comandos do objeto, que aplicam validações.

Próximo capitúlo

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

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

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.