// aula 04 · Python · POO

Melhorando Classes e
Criando uma Conta Bancária


O que vamos construir

Nessa aula o Guanabara mostra como melhorar uma classe existente aplicando boas práticas de POO, e cria do zero uma classe ContaBancaria — um exemplo clássico para entender atributos, métodos, encapsulamento e controle de estado na prática.

A ideia é sair do exemplo da Caneta (abstrato) e partir para algo mais próximo do mundo real: uma conta com titular, saldo, depósitos e saques.

Por que conta bancária? Porque ela tem tudo que uma boa classe precisa: atributos (saldo, titular), métodos (depositar, sacar), validações (não sacar mais do que tem) e encapsulamento (saldo não deve ser alterado diretamente).


Melhorando uma classe — boas práticas

Classe sem boas práticas

Atributos públicos — qualquer código pode alterar o saldo diretamente. Sem validação, sem controle. Isso quebra o encapsulamento.

Ex: conta.saldo = -9999 funcionaria sem erro.

Classe com boas práticas

Atributos privados com __. Acesso controlado por métodos. Validações dentro dos métodos. O objeto cuida da sua própria integridade.

Ex: conta.sacar(200) verifica o saldo antes de permitir.

O que melhorar em uma classe

1. Privacidade

Proteja os dados

Use __atributo para tornar atributos privados. Assim ninguém altera o saldo diretamente de fora da classe.

2. Validação

Controle nos métodos

Coloque if dentro dos métodos para validar antes de executar. Ex: checar se tem saldo antes de sacar.

3. __str__

Representação legível

Defina o método __str__ para que ao fazer print(conta) apareça algo útil ao invés de um endereço de memória.


Estrutura da classe ContaBancaria

Atributos

O que a conta tem

  • __titular — nome do dono da conta
  • __saldo — valor atual disponível
  • __limite — limite de crédito adicional
  • __extrato — histórico de movimentações
Métodos

O que a conta faz

  • depositar(valor) — adiciona ao saldo
  • sacar(valor) — remove com validação
  • transferir(valor, destino) — saca e deposita
  • get_saldo() — retorna o saldo atual
  • __str__() — exibe dados da conta
Método O que faz Validação
depositar(v) Soma v ao saldo e registra no extrato Valor deve ser maior que zero
sacar(v) Subtrai v do saldo e registra no extrato Saldo + limite deve ser suficiente
transferir(v, dest) Saca desta conta e deposita na conta destino Mesma validação do saque
get_saldo() Retorna o valor do saldo atual
get_extrato() Retorna o histórico de movimentações

Código completo — passo a passo

passo 1 — criando a classe e o construtor

class ContaBancaria:
    def __init__(self, titular, saldo_inicial=0, limite=0):
        self.__titular = titular          # privado — nome do titular
        self.__saldo   = saldo_inicial    # privado — saldo inicial
        self.__limite  = limite           # privado — limite de crédito
        self.__extrato = []              # lista para guardar movimentações
        self.__extrato.append(f'Conta criada com saldo R$ {saldo_inicial:.2f}')

O construtor (__init__) é chamado automaticamente quando o objeto é criado. Aqui já definimos os atributos com __ (privados) e criamos uma lista de extrato vazia que vai crescer com cada operação.

passo 2 — método depositar

    def depositar(self, valor):
        if valor > 0:
            self.__saldo += valor
            self.__extrato.append(f'Depósito: R$ {valor:.2f}')
            print(f'✅ Depósito de R$ {valor:.2f} realizado.')
        else:
            print('❌ Valor de depósito inválido.')

passo 3 — método sacar (com validação)

    def sacar(self, valor):
        saldo_disponivel = self.__saldo + self.__limite
        if 0 < valor <= saldo_disponivel:
            self.__saldo -= valor
            self.__extrato.append(f'Saque:    R$ {valor:.2f}')
            print(f'✅ Saque de R$ {valor:.2f} realizado.')
        else:
            print('❌ Saldo insuficiente ou valor inválido.')

passo 4 — método transferir

    def transferir(self, valor, conta_destino):
        saldo_disponivel = self.__saldo + self.__limite
        if 0 < valor <= saldo_disponivel:
            self.__saldo -= valor
            self.__extrato.append(f'Transferência enviada: R$ {valor:.2f}')
            conta_destino.depositar(valor)
            print(f'✅ Transferência de R$ {valor:.2f} realizada.')
        else:
            print('❌ Saldo insuficiente para transferência.')

passo 5 — getters e __str__

    # Getters — acesso controlado aos atributos privados
    def get_saldo(self):   return self.__saldo
    def get_titular(self): return self.__titular

    def get_extrato(self):
        print(f'\n--- Extrato de {self.__titular} ---')
        for mov in self.__extrato:
            print(f'  {mov}')
        print(f'  Saldo atual: R$ {self.__saldo:.2f}')
        print('------------------------\n')

    # __str__ — o que aparece ao fazer print(conta)
    def __str__(self):
        return (f'ContaBancaria | Titular: {self.__titular} | '
                f'Saldo: R$ {self.__saldo:.2f} | Limite: R$ {self.__limite:.2f}')

usando a classe — exemplo completo

# Criando as contas
conta_lucilia = ContaBancaria('Lucilia', saldo_inicial=1000, limite=500)
conta_vini    = ContaBancaria('Vinicius', saldo_inicial=300)

# Operações
conta_lucilia.depositar(500)          # ✅ Depósito de R$ 500.00 realizado.
conta_lucilia.sacar(200)             # ✅ Saque de R$ 200.00 realizado.
conta_lucilia.transferir(100, conta_vini)  # ✅ Transferência realizada.
conta_lucilia.sacar(9999)           # ❌ Saldo insuficiente.

# Ver extrato
conta_lucilia.get_extrato()

# print() usa o __str__
print(conta_lucilia)
# ContaBancaria | Titular: Lucilia | Saldo: R$ 1200.00 | Limite: R$ 500.00

Os pilares da POO na prática — ContaBancaria

Abstração

Simplificamos o mundo real

Uma conta bancária real tem dezenas de atributos. Aqui modelamos só o necessário: titular, saldo, limite e extrato. O resto não importa para o nosso programa.

Encapsulamento

Saldo só muda por métodos

O __saldo é privado. Ninguém faz conta.saldo = -999. Só depositar() e sacar() alteram o saldo — e com validação.

Herança

Pronto para herdar

Poderíamos criar ContaCorrente e ContaPoupanca herdando de ContaBancaria, cada uma com suas particularidades, sem reescrever tudo.

Polimorfismo

Mesmo método, comportamentos diferentes

Uma ContaPoupanca poderia ter seu próprio sacar() com regras diferentes (ex: só 1 saque por mês), mesmo herdando da classe base.



Dunder attributes — métodos especiais do Python

Dunder vem de Double UNDERscore — os métodos com __duplo_underscore__ antes e depois do nome. O Python os chama automaticamente em situações específicas, sem você precisar invocar diretamente.

Você já usou dois nessa aula: __init__ (construtor) e __str__ (representação). Mas existem vários outros que deixam sua classe muito mais poderosa.

Dunder não é nome bonito — é uma convenção do Python para separar métodos especiais (que o interpretador chama) dos métodos normais (que você chama). Nunca crie métodos com __nome__ por conta própria.

__init__

Construtor

Chamado automaticamente quando o objeto é criado. Define os atributos iniciais.

Ativado por: ContaBancaria('Lucilia')

__str__

Representação legível

Chamado quando você faz print(obj) ou str(obj). Deve retornar uma string amigável.

Ativado por: print(conta)

__repr__

Representação técnica

Usado no console interativo e em logs. Deve retornar uma string que idealmente recrie o objeto.

Ativado por: digitar conta no console

__len__

Comprimento

Chamado quando você usa len(obj). Útil para classes que representam coleções.

Ativado por: len(conta)

__eq__

Igualdade

Define o que acontece ao usar == entre dois objetos. Sem ele, Python compara os endereços de memória.

Ativado por: conta1 == conta2

__del__

Destrutor

Chamado quando o objeto é destruído (removido da memória). Raramente necessário no dia a dia.

Ativado por: del conta

exemplo — dunder na ContaBancaria

class ContaBancaria:
    def __init__(self, titular, saldo_inicial=0):
        self.__titular = titular
        self.__saldo   = saldo_inicial
        self.__extrato = []

    # Representação amigável — para print()
    def __str__(self):
        return f'Conta de {self.__titular} | Saldo: R$ {self.__saldo:.2f}'

    # Representação técnica — para o console
    def __repr__(self):
        return f"ContaBancaria('{self.__titular}', {self.__saldo})"

    # Tamanho — retorna quantas movimentações tem no extrato
    def __len__(self):
        return len(self.__extrato)

    # Igualdade — duas contas são iguais se tiverem o mesmo titular
    def __eq__(self, outra):
        return self.__titular == outra.get_titular()

# Testando os dunders
c1 = ContaBancaria('Lucilia', 1000)
c2 = ContaBancaria('Vinicius', 300)

print(c1)           # → Conta de Lucilia | Saldo: R$ 1000.00
repr(c1)           # → ContaBancaria('Lucilia', 1000)
len(c1)            # → 0  (nenhuma movimentação ainda)
print(c1 == c2)   # → False

Erros comuns nessa aula

Erro 1

Atributo público por engano

Esquecer o __ antes do atributo. Com self.saldo (público), qualquer código pode fazer conta.saldo = -1000 sem validação.

Erro 2

Não usar self nos métodos

Todo método de instância precisa de self como primeiro parâmetro. Sem ele o Python não sabe de qual objeto está falando.

Erro 3

Acessar atributo privado direto

Fazer conta.__saldo de fora da classe gera AttributeError. Use sempre o getter: conta.get_saldo().

Erro 4

Esquecer o __str__

Sem __str__, fazer print(conta) mostra algo como <__main__.ContaBancaria object at 0x...> — inútil para debug.