Pular para conteúdo

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
NameError: name 'excedente' is not defined

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!")
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
AssertionError

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!")
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 return devolveu.
  • assert condição verifica algo que tem de ser verdade: silêncio se for, AssertionError na 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 devolve None.

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

Lista 13 · Decompor e testar.

É 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.