13. Decompor e testar¶
Nesta aula
Ideia central: um programa grande fica mais fácil de escrever, ler e
corrigir quando é uma sequência de funções pequenas, cada uma com um
trabalho só, do que quando é um bloco único. E, porque as suas funções
agora devolvem valor, dá para conferir cada uma sozinha — com o
assert, a mesma ferramenta que passa a corrigir as suas listas a
partir de hoje.
Ao fim você vai conseguir: quebrar um programa em funções que se
chamam, em sequência; explicar por que uma variável criada dentro de
uma função não existe fora dela; escrever e interpretar um assert; e
ler um AssertionError até achar a causa.
De um bloco só para várias funções¶
Até o capítulo passado, cada função resolvia uma conta isolada. Um programa de verdade normalmente precisa de várias contas, em sequência, cada uma usando o resultado da anterior. Em vez de escrever tudo dentro de uma função só, cada etapa vira a sua própria função — e um trecho curto no fim só chama cada uma, na ordem certa.
def calcula_excedente(consumo_gb, franquia_gb):
if consumo_gb > franquia_gb:
return consumo_gb - franquia_gb
return 0
def calcula_custo_extra(excedente_gb, preco_por_gb):
return excedente_gb * preco_por_gb
def monta_mensagem(excedente_gb, custo_extra):
if excedente_gb == 0:
return "Consumo dentro da franquia. Nada a cobrar."
return f"Excedeu {excedente_gb} GB. Cobranca extra: R$ {custo_extra:.2f}"
consumo = float(input("Consumo do mes (GB): "))
franquia = float(input("Franquia do plano (GB): "))
preco_gb = float(input("Preco por GB excedente: "))
excedente = calcula_excedente(consumo, franquia)
custo_extra = calcula_custo_extra(excedente, preco_gb)
mensagem = monta_mensagem(excedente, custo_extra)
print(mensagem)
Consumo do mes (GB): 12
Franquia do plano (GB): 10
Preco por GB excedente: 5
Excedeu 2.0 GB. Cobranca extra: R$ 10.00
Três funções, cada uma com um trabalho só: calcula_excedente decide quanto
passou da franquia, calcula_custo_extra transforma isso em dinheiro, e
monta_mensagem decide o texto final. Nenhuma delas sabe da existência das
outras duas — cada uma só recebe o que precisa e devolve o que calculou. Quem
liga as três é o trecho de fora, lendo os dados e passando o resultado de uma
função como argumento da próxima.
💡 Pense assim: a linha de montagem
Cada função é uma estação da linha de montagem: recebe uma peça, faz uma coisa nela, entrega para a próxima estação. Ninguém na linha precisa saber montar o produto inteiro — só a sua parte. E se uma estação tiver defeito, você sabe exatamente qual, porque cada uma faz uma coisa só.
Decompor assim traz uma vantagem que um bloco único não tem: cada função dá para testar sozinha, sem rodar o programa inteiro nem digitar nada no teclado. É para isso que serve a próxima seção.
Escopo: o que existe dentro não existe fora¶
Uma variável criada dentro de uma função só existe enquanto aquela chamada está rodando. Quando a função termina, ela desaparece — mesmo que a função tenha sido chamada.
def calcula_excedente(consumo_gb, franquia_gb):
excedente = consumo_gb - franquia_gb
return excedente
calcula_excedente(12, 10)
print(excedente) # excedente so existe DENTRO da funcao
excedente foi criada dentro de calcula_excedente, e é lá que ela mora.
Chamar a função (calcula_excedente(12, 10)) não "vaza" essa variável para
fora — o return devolveu o valor, não o nome. Quem quiser usar o
resultado fora da função precisa guardá-lo numa variável própria, do jeito que
o capítulo 12 já vinha fazendo: excedente = calcula_excedente(12, 10).
O nome do parâmetro também é local
O mesmo vale para os parâmetros. consumo_gb e franquia_gb só existem
dentro de calcula_excedente, durante aquela chamada. Duas funções
diferentes podem ter um parâmetro com o mesmo nome, e uma nunca vê o
valor da outra — cada chamada tem o seu próprio conjunto de variáveis
locais, isolado de tudo mais.
assert: uma régua dentro do próprio programa¶
Desde a Lista 01 você confere um programa de duas formas: rodando e olhando a
tela, ou — a partir da Lista 05 — comparando com uma sessão publicada. O
assert é uma terceira forma, e é a que passa a valer a partir de hoje.
assert condição verifica uma condição: se ela for True, nada
acontece — o programa segue para a linha seguinte, em silêncio. Se ela for
False, o programa para na hora com um AssertionError.
def dobro(numero):
return numero * 2
assert dobro(5) == 10
assert dobro(0) == 0
print("Passou nos dois testes!")
As duas linhas de assert não imprimem nada — dobro(5) == 10 e dobro(0) ==
0 são True, então o Python simplesmente segue em frente até o print
final. É por isso que "nenhum erro apareceu" já é a resposta: um assert que
passa é silencioso de propósito.
Agora o mesmo assert, contra a função sem return do capítulo passado:
def dobro(numero):
print(numero * 2) # imprime, mas nao devolve -- o mesmo defeito do capitulo 12
assert dobro(5) == 10
dobro(5) imprime 10 na tela — como sempre — e devolve None, porque não
tem return. assert dobro(5) == 10 vira assert None == 10, que é
False, e o programa para com AssertionError. É exatamente esse o
defeito que o assert existe para pegar: a função que "funciona" na tela e
não devolve nada. Sem o assert, esse erro só aparece quando alguém, em
outro lugar do programa, tenta usar um resultado que nunca existiu.
AssertionError é o irmão do erro que não dá erro
Todos os outros erros deste semestre — TypeError, IndexError,
NameError — são o Python reclamando de algo que ele não sabe fazer.
AssertionError é diferente: é você dizendo ao Python "isto tem que
ser verdade", e o programa concordando em parar assim que deixar de ser.
É a ferramenta certa exatamente para o tipo de defeito que não avisa
sozinho.
Um assert sozinho raramente basta. Um exercício de verdade testa mais de um
caso — o normal, a fronteira e o vazio ou zero — porque uma função pode
acertar um caso e errar outro:
def classifica_consumo(consumo_gb, franquia_gb):
if consumo_gb > franquia_gb:
return "excedeu"
return "dentro"
assert classifica_consumo(12, 10) == "excedeu" # caso normal
assert classifica_consumo(10, 10) == "dentro" # caso de fronteira: igual a franquia
assert classifica_consumo(0, 10) == "dentro" # caso vazio: consumo zero
print("Passou nos tres casos!")
Repare nos três casos: 12 contra franquia 10 é o consumo que estourou —
o caso normal; 10 contra franquia 10 é exatamente a fronteira, onde
um > trocado por >= faria diferença; 0 é o caso vazio, o consumo
mais baixo que existe. É exatamente essa régua — normal, fronteira, vazio —
que os testes da Lista 12 e da Lista 13 seguem, e que fica em
teste_exNN.py, sempre no fim do arquivo. Não altere essa parte: ela é a
régua, não o seu trabalho.
Erros comuns deste capítulo¶
| Mensagem ou sintoma | Causa provável |
|---|---|
NameError: name 'x' is not defined (fora de uma função) |
a variável foi criada dentro de uma função; ela não existe fora |
AssertionError sozinho, sem mais nada |
um assert falhou — comece conferindo se a função tem return |
o AssertionError aponta a linha certa, mas o valor não bate |
erro de lógica dentro da função — releia a fórmula do enunciado |
| o teste passa nos casos comuns e falha só na fronteira | comparação usa > onde devia ser >=, ou o contrário |
SyntaxError na linha do assert |
faltou fechar um parêntese, ou a comparação ficou incompleta |
Resumo¶
- Decompor: cada função cuida de um pedaço do problema; um trecho curto no fim só chama cada uma, na ordem certa, passando o resultado de uma como entrada da próxima.
- Escopo: uma variável (ou parâmetro) criado dentro de uma função só
existe ali dentro. Fora dela, use o valor que o
returndevolveu. assert condiçãoverifica algo que tem de ser verdade: silêncio se for,AssertionErrorna hora se não for.- Um teste de verdade cobre mais de um caso: o normal, a fronteira e o vazio ou zero.
- O
asserté a ferramenta certa contra o defeito que abriu esta unidade: a função que mostra o número certo na tela e devolveNone.
O fecho da disciplina¶
Dezessete semanas atrás, a primeira pergunta desta disciplina foi "o que é um algoritmo?" — uma sequência de passos, precisa o bastante para um computador seguir. Hoje esse "passo" virou função: um pedaço nomeado, testável sozinho, que outra função pode chamar sem saber como ele funciona por dentro. É a mesma ideia do capítulo 1, só que agora você sabe empacotá-la, reutilizá-la e provar que ela está certa — sem esperar alguém olhar a tela e confiar.
Se o período seguinte da sua grade for Python para Telecomunicações, o que vem depois está mapeado, item por item, em O que vem depois — vale a leitura antes da primeira aula de lá.
Para praticar¶
É a última lista do semestre. Alguns exercícios pedem duas funções no mesmo arquivo, uma chamando a outra — a mesma decomposição desta aula, só que agora é você quem decide onde cortar o problema.