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.
- Dicionários e funções, na versão 1.
- Classes e objetos, __init__ e self e métodos, na versão 2.
- Encapsulamento, @property e raise, na versão 3.
- Herança, polimorfismo, exceções próprias e try e except, na versão 4.
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:
- Dicionários e funções: funciona, mas os dados ficam soltos.
- Uma classe: dados e ações juntos, com extrato.
- @property e validação: o saldo só muda pelos métodos, e valores absurdos são recusados.
- 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
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.
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
sacarnã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
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.saldocontinua 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.
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 emError, 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 comsuper()e acrescenta ataxa. - 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:
ContaBancariaouContaPoupanca. O mesmomostrar_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
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ão | Como a conta é guardada | Saque sem saldo | Saldo mudado por fora | Valor negativo |
|---|---|---|---|---|
| 1 · Dicionário | {"titular": …, "saldo": …} + funções | Mensagem na tela | Aceito, calado | Aceito |
| 2 · Classe | Objeto com atributos e métodos | return False | Aceito, calado | Aceito |
| 3 · @property | _saldo + propriedade só de leitura | return False | AttributeError | ValueError |
| 4 · Herança + exceção | Igual à 3, mais a ContaPoupanca | SaldoInsuficienteError | AttributeError | ValueError |
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)cujofim_do_mescobra R$ 5,00 de tarifa (registrada no extrato comoTarifa). E se o saldo for menor que a tarifa? - Cheque especial: dê à
ContaCorrenteumlimite, e sobrescreva osacarpara aceitar saques atésaldo + limite. - Transferência com exceção: leve o
transferirda versão 2 para a versão 4. O que acontece com o dinheiro se o saque levantarSaldoInsuficienteError? (Dica: o depósito no destino nem chega a rodar.) - Extrato em arquivo: faça o
_registrartambém gravar cada linha numextrato.txt(aula Escrevendo Arquivos). Repare que nada fora da classe precisa mudar.
Referência rápida do projeto
| Peça | O que faz | Aula |
|---|---|---|
class ContaBancaria: + __init__ | O modelo de toda conta | __init__ e self |
self.extrato = [] no __init__ | Uma lista por conta | Atributos e Métodos |
self._saldo | Dado interno | Encapsulamento |
@property def saldo | Saldo só de leitura | @property |
raise ValueError(…) | Recusa valor absurdo | raise |
class SaldoInsuficienteError(Exception) | Exceção própria com dados | Criando suas Exceções |
class ContaPoupanca(ContaBancaria) + super() | Conta filha com juros | Herança |
for conta in contas: conta.fim_do_mes() | Cada conta do seu jeito | Polimorfismo |
Exercícios
1. A mãe das exceções
Complete a classe-mãe de uma exceção própria:
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+@propertysem setter deixam o saldo só de leitura;raise ValueErrorrecusa valores absurdos; o saldo inicial entra como depósito. - Versão 4:
SaldoInsuficienteErrorcarrega o saldo e o valor; aContaPoupancaherda tudo e sobrescreve só ofim_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.