Python Projeto: Conta Bancária

O banco do bairro pediu à Ana um programinha para controlar as contas dos clientes: abrir conta, depositar, sacar, ver o extrato e, para quem tem poupança, render os juros no fim do mês. Neste projeto você vai construir esse programa quatro vezes, cada vez melhor. A primeira versão usa só o que você aprendeu no Nível 2 (dicionários e funções); as outras vão trazendo, uma por vez, as ferramentas da Orientação a Objetos. No fim, você vai ter visto na prática por que cada uma delas existe.

Neste projeto você vai usar

O que vamos construir

Esta é a versão final rodando no computador da Ana. A Ana tem uma conta corrente; a Bia, uma poupança. O programa faz alguns depósitos e saques (um deles sem saldo), fecha o mês e mostra o extrato de cada uma:

$ python3 banco.py
Bia: saque negado (saldo de R$ 1000.00, saque de R$ 2000.00)
  faltaram R$ 1000.00
--- ContaBancaria de Ana ---
Depósito     +100.00
Depósito      +50.00
Saque         -30.00
Saldo         120.00
--- ContaPoupanca de Bia ---
Depósito    +1000.00
Saque        -200.00
Juros          +8.00                « a poupança rendeu 1% no fim do mês
Saldo         808.00

As quatro versões:

  1. Dicionários e funções: funciona, mas os dados ficam soltos.
  2. Uma classe: dados e ações juntos, com extrato.
  3. @property e validação: o saldo só muda pelos métodos, e valores absurdos são recusados.
  4. Herança e uma exceção própria: a poupança e o SaldoInsuficienteError.

Antes de começar

Crie a pasta do projeto. O arquivo vai se chamar banco.py, e cada versão substitui a anterior:

$ mkdir -p projetos/banco
$ cd projetos/banco

Em cada versão há uma caixa Experimente, que roda Python de verdade aqui na página. As missões usam uma versão enxuta da conta, só com o que aquele passo precisa.


Versão 1 · Dicionários e funções

Com o que você já sabe, uma conta pode ser um dicionário com o titular e o saldo, e as operações podem ser funções que recebem a conta:

def criar_conta(titular, saldo=0):
    return {"titular": titular, "saldo": saldo}


def depositar(conta, valor):
    conta["saldo"] += valor


def sacar(conta, valor):
    if valor > conta["saldo"]:
        print(f"Saque de R$ {valor:.2f} negado: saldo insuficiente.")
    else:
        conta["saldo"] -= valor


def mostrar(conta):
    print(f"{conta['titular']}: R$ {conta['saldo']:.2f}")


ana = criar_conta("Ana", 100)
bia = criar_conta("Bia")

depositar(ana, 50)
sacar(ana, 30)
depositar(bia, 80)
sacar(bia, 200)

mostrar(ana)
mostrar(bia)
criar_conta(titular, saldo=0)
Monta e devolve o dicionário de uma conta. O saldo começa em 0 se ninguém disser nada.
conta["saldo"] += valor
As funções mudam o dicionário que receberam. Como dicionários são mutáveis, a mudança vale fora da função (aula Mutável × Imutável).
sacar(conta, valor)
A regra do banco: não se saca mais do que se tem.
conta['titular']
Dentro da f-string, aspas simples, porque a f-string já usa as duplas.
$ python3 banco.py
Saque de R$ 200.00 negado: saldo insuficiente.
Ana: R$ 120.00
Bia: R$ 80.00

Funciona. Mas repare em duas fraquezas. Os dados (o dicionário) e as ações (as funções) vivem separados: nada no dicionário diz que ele "sabe" sacar. E o dicionário aceita qualquer coisa, como você vai ver agora.

Quando dá errado na versão 1

Uma chave digitada errado: KeyError
def criar_conta(titular, saldo=0):
    return {"titular": titular, "saldo": saldo}


def depositar(conta, valor):
    conta["sado"] += valor


ana = criar_conta("Ana", 100)
depositar(ana, 50)
$ python3 banco.py
Traceback (most recent call last):
  File "/home/ana/projetos/banco/banco.py", line 10, in <module>
    depositar(ana, 50)
    ~~~~~~~~~^^^^^^^^^
  File "/home/ana/projetos/banco/banco.py", line 6, in depositar
    conta["sado"] += valor
    ~~~~~^^^^^^^^
KeyError: 'sado'

Faltou um l: "sado". O erro só aparece quando o programa roda aquela linha, e cada função que mexe na conta é um lugar a mais para errar o nome da chave.

Qualquer um muda o saldo
def criar_conta(titular, saldo=0):
    return {"titular": titular, "saldo": saldo}


def mostrar(conta):
    print(f"{conta['titular']}: R$ {conta['saldo']:.2f}")


ana = criar_conta("Ana", 100)
ana["saldo"] = -1000
mostrar(ana)
$ python3 banco.py
Ana: R$ -1000.00

Nenhum erro. A função sacar tem a regra do saldo, mas ninguém é obrigado a usá-la. Vamos resolver isso aos poucos: primeiro juntando dados e ações numa classe.

Missão: falta a função sacar(conta, valor). Escreva: se o valor for maior que o saldo, mostre uma mensagem com a palavra negado; se não, tire o valor do saldo. A Ana tem 100, tenta sacar 500 (negado) e depois 30: deve sobrar Ana: R$ 70.00.

def criar_conta(titular, saldo=0):
    return {"titular": titular, "saldo": saldo}


def depositar(conta, valor):
    conta["saldo"] += valor


# escreva aqui a função sacar(conta, valor)


def mostrar(conta):
    print(f"{conta['titular']}: R$ {conta['saldo']:.2f}")


ana = criar_conta("Ana", 100)
sacar(ana, 500)
sacar(ana, 30)
mostrar(ana)
def criar_conta(titular, saldo=0):
    return {"titular": titular, "saldo": saldo}


def depositar(conta, valor):
    conta["saldo"] += valor


def sacar(conta, valor):
    if valor > conta["saldo"]:
        print(f"Saque de R$ {valor:.2f} negado: saldo insuficiente.")
    else:
        conta["saldo"] -= valor


def mostrar(conta):
    print(f"{conta['titular']}: R$ {conta['saldo']:.2f}")


ana = criar_conta("Ana", 100)
sacar(ana, 500)
sacar(ana, 30)
mostrar(ana)

Versão 2 · Uma classe

Agora a conta vira uma classe. O que ela tem (titular, saldo, extrato) e o que ela faz (depositar, sacar, mostrar o extrato) ficam juntos:

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo
        self.extrato = []

    def depositar(self, valor):
        self.saldo += valor
        self.extrato.append(f"Depósito: +{valor:.2f}")

    def sacar(self, valor):
        if valor > self.saldo:
            self.extrato.append(f"Saque negado: {valor:.2f}")
            return False
        self.saldo -= valor
        self.extrato.append(f"Saque: -{valor:.2f}")
        return True

    def mostrar_extrato(self):
        print(f"--- Extrato de {self.titular} ---")
        for linha in self.extrato:
            print(linha)
        print(f"Saldo: R$ {self.saldo:.2f}")


ana = ContaBancaria("Ana", 100)
bia = ContaBancaria("Bia")

ana.depositar(50)
ana.sacar(30)
bia.depositar(80)
if not bia.sacar(200):
    print("A Bia não conseguiu sacar R$ 200.00")

ana.mostrar_extrato()
bia.mostrar_extrato()
self.extrato = []
Cada conta nasce com a sua lista de movimentos, criada no __init__. (Na classe, fora do __init__, a lista seria uma só para todas as contas: veja Atributos e Métodos.)
self.extrato.append(…)
Todo depósito e todo saque deixam um registro. A lista guarda textos já formatados.
return False / return True
O sacar não imprime nada: ele responde se deu certo. Quem chamou decide o que fazer.
if not bia.sacar(200):
Chama o saque e, se ele devolveu False, avisa.
$ python3 banco.py
A Bia não conseguiu sacar R$ 200.00
--- Extrato de Ana ---
Depósito: +50.00
Saque: -30.00
Saldo: R$ 120.00
--- Extrato de Bia ---
Depósito: +80.00
Saque negado: 200.00
Saldo: R$ 80.00

Compare a chamada: sacar(ana, 30) virou ana.sacar(30). A conta agora sabe sacar, e o nome saldo é escrito num lugar só (a classe), em vez de em cada função. Errar o nome de um atributo ainda dá erro, mas dentro de um arquivo só, perto de tudo o que a conta faz.

Quando dá errado na versão 2

O saldo continua aberto
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo
        self.extrato = []

    def depositar(self, valor):
        self.saldo += valor
        self.extrato.append(f"Depósito: +{valor:.2f}")


ana = ContaBancaria("Ana", 100)
ana.depositar(-500)
ana.saldo = 1_000_000
print(ana.saldo)
print(ana.extrato)
$ python3 banco.py
1000000
['Depósito: +-500.00']

Dois furos: o depositar aceitou um depósito negativo (repare no +-500.00 do extrato), e o ana.saldo = 1_000_000 mudou o saldo por fora, sem registro nenhum no extrato. A versão 3 fecha os dois.

Missão: o banco quer transferências. Escreva o método transferir(self, destino, valor): ele tenta sacar o valor desta conta e, só se o saque der certo, deposita na conta de destino. Use os métodos que já existem. A Ana transfere 30 para a Bia (dá certo) e depois 500 (não dá): deve ficar Ana: R$ 70.00 e Bia: R$ 30.00.

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo
        self.extrato = []

    def depositar(self, valor):
        self.saldo += valor
        self.extrato.append(f"Depósito: +{valor:.2f}")

    def sacar(self, valor):
        if valor > self.saldo:
            self.extrato.append(f"Saque negado: {valor:.2f}")
            return False
        self.saldo -= valor
        self.extrato.append(f"Saque: -{valor:.2f}")
        return True

    # escreva aqui o método transferir(self, destino, valor)


ana = ContaBancaria("Ana", 100)
bia = ContaBancaria("Bia")

ana.transferir(bia, 30)
ana.transferir(bia, 500)

print(f"Ana: R$ {ana.saldo:.2f}")
print(f"Bia: R$ {bia.saldo:.2f}")
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self.saldo = saldo
        self.extrato = []

    def depositar(self, valor):
        self.saldo += valor
        self.extrato.append(f"Depósito: +{valor:.2f}")

    def sacar(self, valor):
        if valor > self.saldo:
            self.extrato.append(f"Saque negado: {valor:.2f}")
            return False
        self.saldo -= valor
        self.extrato.append(f"Saque: -{valor:.2f}")
        return True

    def transferir(self, destino, valor):
        if self.sacar(valor):
            destino.depositar(valor)
            return True
        return False


ana = ContaBancaria("Ana", 100)
bia = ContaBancaria("Bia")

ana.transferir(bia, 30)
ana.transferir(bia, 500)

print(f"Ana: R$ {ana.saldo:.2f}")
print(f"Bia: R$ {bia.saldo:.2f}")

Repare como o transferir ficou curto: ele não repete a regra do saldo, só usa o sacar. Se um dia a regra do saque mudar, a transferência acompanha sozinha.


Versão 3 · @property e validação

Agora o saldo passa a morar em self._saldo (com sublinhado: "uso interno", aula Encapsulamento), e quem está de fora o enxerga por uma propriedade só de leitura (aula @property). Os métodos passam a recusar valores absurdos com ValueError:

class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self._saldo = 0
        self._extrato = []
        if saldo:
            self.depositar(saldo)

    @property
    def saldo(self):
        return self._saldo

    def depositar(self, valor):
        if valor <= 0:
            raise ValueError(f"depósito precisa ser maior que zero (veio {valor})")
        self._saldo += valor
        self._extrato.append(f"Depósito: +{valor:.2f}")

    def sacar(self, valor):
        if valor <= 0:
            raise ValueError(f"saque precisa ser maior que zero (veio {valor})")
        if valor > self._saldo:
            self._extrato.append(f"Saque negado: {valor:.2f}")
            return False
        self._saldo -= valor
        self._extrato.append(f"Saque: -{valor:.2f}")
        return True

    def mostrar_extrato(self):
        print(f"--- Extrato de {self.titular} ---")
        for linha in self._extrato:
            print(linha)
        print(f"Saldo: R$ {self.saldo:.2f}")


ana = ContaBancaria("Ana", 100)
ana.sacar(30)
for valor in [50, -500, 0]:
    try:
        ana.depositar(valor)
    except ValueError as erro:
        print(f"Recusado: {erro}")

print(f"Saldo da Ana: R$ {ana.saldo:.2f}")
ana.mostrar_extrato()
ana.saldo = 1_000_000
self._saldo = 0 … self.depositar(saldo)
A conta nasce zerada, e o saldo inicial entra como um depósito. Assim ele passa pela mesma validação e aparece no extrato.
@property def saldo(self)
De fora, ana.saldo continua funcionando para ler. Como não há setter, ninguém consegue escrever.
raise ValueError(…)
Depósito ou saque de zero ou menos não fazem sentido: o método se recusa a continuar e explica por quê.
try: … except ValueError as erro:
Quem usa a conta trata a recusa e o programa segue.
ana.saldo = 1_000_000
A última linha tenta a fraude da versão 2. Veja o que acontece agora.
$ python3 banco.py
Recusado: depósito precisa ser maior que zero (veio -500)
Recusado: depósito precisa ser maior que zero (veio 0)
Saldo da Ana: R$ 120.00
--- Extrato de Ana ---
Depósito: +100.00                  « o saldo inicial entrou como depósito
Saque: -30.00
Depósito: +50.00
Saldo: R$ 120.00
Traceback (most recent call last):
  File "/home/ana/projetos/banco/banco.py", line 46, in <module>
    ana.saldo = 1_000_000
    ^^^^^^^^^
AttributeError: property 'saldo' of 'ContaBancaria' object has no setter

Os depósitos de -500 e de 0 foram recusados, o extrato agora conta a história inteira (inclusive o saldo de abertura), e a fraude, que antes passava calada, agora para o programa com AttributeError: property 'saldo' … has no setter, "a propriedade saldo não tem setter". Este erro é o programa funcionando como deve: é o cadeado da versão 3.

Por que não um setter com validação?

Daria para escrever um @saldo.setter que recusasse valores negativos (a missão final da aula @property faz isso). Aqui a escolha é outra: num banco, o saldo nunca é escrito direto; ele é o resultado de depósitos e saques, e cada um deixa registro. Sem setter, a única porta para mudar o saldo são os métodos.

Missão: o depositar desta conta aceita qualquer valor, e o depósito de -500 deixa o saldo negativo. Faça o método levantar um ValueError quando o valor for zero ou menos. O código de baixo já trata o erro: deve aparecer uma linha começando com Recusado: e, no fim, Saldo: R$ 150.00.

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

    @property
    def saldo(self):
        return self._saldo

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


ana = ContaBancaria("Ana", 100)
for valor in [50, -500]:
    try:
        ana.depositar(valor)
    except ValueError as erro:
        print(f"Recusado: {erro}")

print(f"Saldo: R$ {ana.saldo:.2f}")
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self._saldo = saldo

    @property
    def saldo(self):
        return self._saldo

    def depositar(self, valor):
        if valor <= 0:
            raise ValueError(f"depósito precisa ser maior que zero (veio {valor})")
        self._saldo += valor


ana = ContaBancaria("Ana", 100)
for valor in [50, -500]:
    try:
        ana.depositar(valor)
    except ValueError as erro:
        print(f"Recusado: {erro}")

print(f"Saldo: R$ {ana.saldo:.2f}")

Versão 4 · Herança e uma exceção própria

Duas novidades. A primeira: o saque sem saldo vira uma exceção própria, SaldoInsuficienteError (aula Criando suas Exceções). Um return False é fácil de ignorar; uma exceção não. A segunda: a poupança vira uma classe filha da conta, que só acrescenta os juros (aula Herança).

class SaldoInsuficienteError(Exception):
    def __init__(self, saldo, valor):
        self.saldo = saldo
        self.valor = valor
        super().__init__(f"saldo de R$ {saldo:.2f}, saque de R$ {valor:.2f}")


class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self._saldo = 0
        self._extrato = []
        if saldo:
            self.depositar(saldo)

    @property
    def saldo(self):
        return self._saldo

    def _registrar(self, tipo, valor):
        self._extrato.append(f"{tipo:<10}{valor:>+10.2f}")

    def depositar(self, valor):
        if valor <= 0:
            raise ValueError(f"depósito precisa ser maior que zero (veio {valor})")
        self._saldo += valor
        self._registrar("Depósito", valor)

    def sacar(self, valor):
        if valor <= 0:
            raise ValueError(f"saque precisa ser maior que zero (veio {valor})")
        if valor > self._saldo:
            raise SaldoInsuficienteError(self._saldo, valor)
        self._saldo -= valor
        self._registrar("Saque", -valor)

    def fim_do_mes(self):
        pass

    def mostrar_extrato(self):
        print(f"--- {type(self).__name__} de {self.titular} ---")
        for linha in self._extrato:
            print(linha)
        print(f"{'Saldo':<10}{self.saldo:>10.2f}")


class ContaPoupanca(ContaBancaria):
    def __init__(self, titular, saldo=0, taxa=0.01):
        super().__init__(titular, saldo)
        self.taxa = taxa

    def fim_do_mes(self):
        rendimento = round(self._saldo * self.taxa, 2)
        if rendimento > 0:
            self._saldo += rendimento
            self._registrar("Juros", rendimento)


ana = ContaBancaria("Ana", 100)
bia = ContaPoupanca("Bia", 1000)

ana.depositar(50)
for conta, valor in [(ana, 30), (bia, 2000), (bia, 200)]:
    try:
        conta.sacar(valor)
    except SaldoInsuficienteError as erro:
        print(f"{conta.titular}: saque negado ({erro})")
        print(f"  faltaram R$ {erro.valor - erro.saldo:.2f}")

for conta in [ana, bia]:
    conta.fim_do_mes()
    conta.mostrar_extrato()

As partes novas, uma por uma

class SaldoInsuficienteError(Exception):
Uma exceção é uma classe filha de Exception. O nome termina em Error, como as do Python (ValueError, KeyError): é o costume da PEP 8.
self.saldo = saldo / self.valor = valor
A exceção carrega dados: quem a captura sabe quanto havia e quanto foi pedido, e pode calcular quanto faltou.
super().__init__(f"saldo de R$ …")
A mensagem que aparece quando a exceção é mostrada (no print(erro) ou no traceback).
raise SaldoInsuficienteError(self._saldo, valor)
No lugar do return False. O saque não "responde que não deu": ele se recusa a terminar.
def _registrar(self, tipo, valor):
Um método interno (com sublinhado) que monta cada linha do extrato. O {tipo:<10} alinha o texto à esquerda em 10 colunas; o {valor:>+10.2f} alinha o número à direita em 10 colunas, com duas casas e sempre com sinal (+ ou -). Veja Formatação.
def fim_do_mes(self): pass
Na conta comum, o fim do mês não faz nada. O método existe para que toda conta tenha um fim_do_mes().
class ContaPoupanca(ContaBancaria):
A poupança herda tudo: depositar, sacar, extrato, a propriedade saldo. O __init__ dela chama o da mãe com super() e acrescenta a taxa.
def fim_do_mes(self): (na poupança)
Sobrescreve o da mãe: calcula os juros, arredondados em centavos com round(…, 2), e registra no extrato.
type(self).__name__
O nome da classe do objeto: ContaBancaria ou ContaPoupanca. O mesmo mostrar_extrato, herdado, mostra o tipo certo de cada conta.
for conta in [ana, bia]: conta.fim_do_mes()
Polimorfismo (aula Polimorfismo): o laço trata as duas contas igual, e cada uma faz o seu fim de mês.
$ python3 banco.py
Bia: saque negado (saldo de R$ 1000.00, saque de R$ 2000.00)
  faltaram R$ 1000.00
--- ContaBancaria de Ana ---
Depósito     +100.00
Depósito      +50.00
Saque         -30.00
Saldo         120.00
--- ContaPoupanca de Bia ---
Depósito    +1000.00
Saque        -200.00
Juros          +8.00
Saldo         808.00

Siga a Bia: abriu com 1000, tentou sacar 2000 (a exceção foi capturada, e o erro.valor - erro.saldo calculou que faltaram 1000), sacou 200 e ficou com 800. No fim do mês, a poupança rendeu 1% de 800, os R$ 8,00 de juros. A conta da Ana não rendeu nada: o fim_do_mes dela é o da mãe, que não faz nada.

Quando dá errado na versão 4

Ninguém capturou a exceção

Se o saque sem saldo acontecer fora de um try, o programa para, e o traceback mostra a sua exceção:

class SaldoInsuficienteError(Exception):
    def __init__(self, saldo, valor):
        self.saldo = saldo
        self.valor = valor
        super().__init__(f"saldo de R$ {saldo:.2f}, saque de R$ {valor:.2f}")


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

    def sacar(self, valor):
        if valor > self._saldo:
            raise SaldoInsuficienteError(self._saldo, valor)
        self._saldo -= valor


bia = ContaBancaria("Bia", 1000)
bia.sacar(2000)
$ python3 banco.py
Traceback (most recent call last):
  File "/home/ana/projetos/banco/banco.py", line 20, in <module>
    bia.sacar(2000)
    ~~~~~~~~~^^^^^^
  File "/home/ana/projetos/banco/banco.py", line 15, in sacar
    raise SaldoInsuficienteError(self._saldo, valor)
SaldoInsuficienteError: saldo de R$ 1000.00, saque de R$ 2000.00

O rastro mostra o caminho: a linha 20 chamou o sacar, e a linha 15, dentro dele, levantou a exceção. A última linha tem o nome da exceção e a mensagem montada no super().__init__. Compare com a versão 2: lá, um bia.sacar(2000) sem if devolvia False e ninguém ficava sabendo. Como consertar: envolva o saque num try com except SaldoInsuficienteError, como no programa da versão 4.

Missão: a exceção já existe e o código de baixo já sabe capturá-la, mas o sacar ainda só faz um return calado quando falta saldo, e o saque de 2000 some sem aviso. Troque o return por um raise SaldoInsuficienteError(…), passando o saldo e o valor. Deve aparecer Negado: faltaram R$ 1000.00.

class SaldoInsuficienteError(Exception):
    def __init__(self, saldo, valor):
        self.saldo = saldo
        self.valor = valor
        super().__init__(f"saldo de R$ {saldo:.2f}, saque de R$ {valor:.2f}")


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

    @property
    def saldo(self):
        return self._saldo

    def sacar(self, valor):
        if valor > self._saldo:
            return  # troque por: levante a SaldoInsuficienteError
        self._saldo -= valor


bia = ContaBancaria("Bia", 1000)
try:
    bia.sacar(2000)
except SaldoInsuficienteError as erro:
    print(f"Negado: faltaram R$ {erro.valor - erro.saldo:.2f}")
print(f"Saldo: R$ {bia.saldo:.2f}")
class SaldoInsuficienteError(Exception):
    def __init__(self, saldo, valor):
        self.saldo = saldo
        self.valor = valor
        super().__init__(f"saldo de R$ {saldo:.2f}, saque de R$ {valor:.2f}")


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

    @property
    def saldo(self):
        return self._saldo

    def sacar(self, valor):
        if valor > self._saldo:
            raise SaldoInsuficienteError(self._saldo, valor)
        self._saldo -= valor


bia = ContaBancaria("Bia", 1000)
try:
    bia.sacar(2000)
except SaldoInsuficienteError as erro:
    print(f"Negado: faltaram R$ {erro.valor - erro.saldo:.2f}")
print(f"Saldo: R$ {bia.saldo:.2f}")

Missão: a ContaPoupanca herdou o fim_do_mes da mãe, que não faz nada, e o saldo da Bia não rende. Sobrescreva o fim_do_mes na poupança: some ao self._saldo o saldo vezes a self.taxa (arredonde com round(…, 2)). Com 1000 reais a 1%, deve aparecer Saldo: R$ 1010.00.

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

    @property
    def saldo(self):
        return self._saldo

    def fim_do_mes(self):
        pass


class ContaPoupanca(ContaBancaria):
    def __init__(self, titular, saldo=0, taxa=0.01):
        super().__init__(titular, saldo)
        self.taxa = taxa

    # escreva aqui o fim_do_mes da poupança


bia = ContaPoupanca("Bia", 1000)
bia.fim_do_mes()
print(f"Saldo: R$ {bia.saldo:.2f}")
class ContaBancaria:
    def __init__(self, titular, saldo=0):
        self.titular = titular
        self._saldo = saldo

    @property
    def saldo(self):
        return self._saldo

    def fim_do_mes(self):
        pass


class ContaPoupanca(ContaBancaria):
    def __init__(self, titular, saldo=0, taxa=0.01):
        super().__init__(titular, saldo)
        self.taxa = taxa

    def fim_do_mes(self):
        self._saldo += round(self._saldo * self.taxa, 2)


bia = ContaPoupanca("Bia", 1000)
bia.fim_do_mes()
print(f"Saldo: R$ {bia.saldo:.2f}")

As quatro versões lado a lado

VersãoComo a conta é guardadaSaque sem saldoSaldo mudado por foraValor negativo
1 · Dicionário{"titular": …, "saldo": …} + funçõesMensagem na telaAceito, caladoAceito
2 · ClasseObjeto com atributos e métodosreturn FalseAceito, caladoAceito
3 · @property_saldo + propriedade só de leiturareturn FalseAttributeErrorValueError
4 · Herança + exceçãoIgual à 3, mais a ContaPoupancaSaldoInsuficienteErrorAttributeErrorValueError

Não existe uma versão "errada": a versão 1 é ótima para um script de dez linhas. O que muda é o tamanho do programa e quantas pessoas mexem nele. Quanto maior, mais vale a pena que o próprio código impeça os enganos, em vez de confiar que ninguém vai cometê-los.


Desafios extras

  • Tarifa da conta corrente: crie ContaCorrente(ContaBancaria) cujo fim_do_mes cobra R$ 5,00 de tarifa (registrada no extrato como Tarifa). E se o saldo for menor que a tarifa?
  • Cheque especial: dê à ContaCorrente um limite, e sobrescreva o sacar para aceitar saques até saldo + limite.
  • Transferência com exceção: leve o transferir da versão 2 para a versão 4. O que acontece com o dinheiro se o saque levantar SaldoInsuficienteError? (Dica: o depósito no destino nem chega a rodar.)
  • Extrato em arquivo: faça o _registrar também gravar cada linha num extrato.txt (aula Escrevendo Arquivos). Repare que nada fora da classe precisa mudar.

Referência rápida do projeto

PeçaO que fazAula
class ContaBancaria: + __init__O modelo de toda conta__init__ e self
self.extrato = [] no __init__Uma lista por contaAtributos e Métodos
self._saldoDado internoEncapsulamento
@property def saldoSaldo só de leitura@property
raise ValueError(…)Recusa valor absurdoraise
class SaldoInsuficienteError(Exception)Exceção própria com dadosCriando suas Exceções
class ContaPoupanca(ContaBancaria) + super()Conta filha com jurosHerança
for conta in contas: conta.fim_do_mes()Cada conta do seu jeitoPolimorfismo

Exercícios

1. A mãe das exceções

Complete a classe-mãe de uma exceção própria:

class SaldoInsuficienteError():

2. Lendo o saldo protegido

Complete o decorador que deixa conta.saldo funcionar só para leitura:


def saldo(self):
    return self._saldo

3. Na versão 3, por que o saldo inicial entra com self.depositar(saldo), e não com self._saldo = saldo?

4. Qual a vantagem do SaldoInsuficienteError sobre o return False da versão 2?

5. Na versão 4, por que a conta da Ana não rendeu juros no fim do mês?

6. A versão 3 parou com AttributeError: property 'saldo' of 'ContaBancaria' object has no setter. O que isso indica?


Resumo

  • Versão 1: dicionário e funções funcionam, mas os dados ficam soltos e qualquer linha muda o saldo.
  • Versão 2: a classe junta dados e ações (ana.sacar(30)); cada conta tem o seu extrato, criado no __init__.
  • Versão 3: _saldo + @property sem setter deixam o saldo só de leitura; raise ValueError recusa valores absurdos; o saldo inicial entra como depósito.
  • Versão 4: SaldoInsuficienteError carrega o saldo e o valor; a ContaPoupanca herda tudo e sobrescreve só o fim_do_mes; um laço trata todas as contas igual (polimorfismo).
  • Cada ferramenta da Orientação a Objetos resolveu um problema que a versão anterior tinha de verdade.