Pular para conteúdo

7. while: repetir enquanto

Nesta aula

Ideia central: um laço é uma condição que o programa volta a testar. O if pergunta uma vez; o while pergunta, executa, e pergunta de novo — até a resposta virar falsa.

Ao fim você vai conseguir: escrever um while com contador e com acumulador; parar um laço quando a pessoa digita um valor combinado (sentinela); rastrear no papel o valor das variáveis volta a volta; e reconhecer as duas maneiras de um laço nunca terminar.

O problema

Este programa avisa o cliente sobre o consumo dos cinco primeiros dias do mês:

# O aviso de consumo dos 5 primeiros dias, sem laco nenhum.
print("Dia 1: verifique seu consumo")
print("Dia 2: verifique seu consumo")
print("Dia 3: verifique seu consumo")
print("Dia 4: verifique seu consumo")
print("Dia 5: verifique seu consumo")
Dia 1: verifique seu consumo
Dia 2: verifique seu consumo
Dia 3: verifique seu consumo
Dia 4: verifique seu consumo
Dia 5: verifique seu consumo

Funciona. Agora imagine o mês inteiro: trinta linhas quase iguais, e quando o texto do aviso mudar você tem de trocar trinta vezes — e vai esquecer uma.

Pior: e se o número de dias só for conhecido quando o programa rodar? Não dá para escrever "N linhas" num arquivo que já está escrito.

Repetir é trabalho de computador. O que o programa precisa é de uma forma de dizer "faça isto de novo enquanto ainda fizer sentido".

A estrutura do while

# O MESMO aviso, com um contador e um while.
dia = 1

while dia <= 5:
    print(f"Dia {dia}: verifique seu consumo")
    dia = dia + 1

print("Fim da semana.")
Dia 1: verifique seu consumo
Dia 2: verifique seu consumo
Dia 3: verifique seu consumo
Dia 4: verifique seu consumo
Dia 5: verifique seu consumo
Fim da semana.

Cinco linhas produziram a mesma saída das seis anteriores — e produziriam trinta linhas trocando um único número.

A forma é quase a do if que você viu na Unidade 2:

while condição:
    o que se repete
    (indentado)

A diferença é uma palavra e é a palavra toda: o if testa uma vez, o while volta. Quando o bloco indentado termina, o programa não segue em frente — ele sobe de volta para a linha do while e testa a condição outra vez. Só quando o teste der False é que a execução continua na primeira linha não indentada.

💡 Pense assim: a escada de um prédio

"Enquanto eu não estiver no meu andar, suba mais um degrau."

A cada degrau você faz a mesma pergunta — cheguei? — e o que muda é a sua posição. Se a sua posição não mudasse, você ficaria perguntando para sempre, parado no mesmo degrau. É exatamente o que acontece com um laço sem a linha que faz a variável avançar.

As três peças

Todo while que termina tem estas três, e a maioria dos laços quebrados é um laço a que falta uma delas:

Peça No exemplo acima O que acontece sem ela
início — a variável existe antes do laço dia = 1 NameError na primeira volta
teste — a condição de continuar while dia <= 5: não é um laço
avanço — algo muda dentro do bloco dia = dia + 1 roda para sempre

O dia = dia + 1 é o mesmo = do capítulo 2: calcule o lado direito e guarde no nome da esquerda. Ele não é uma equação; é o degrau da escada.

Rastrear volta a volta

Esta é a habilidade central do capítulo, e é o que a prova cobra. Rastrear um laço é a mesma tabela do capítulo 2, com uma linha por volta em vez de uma linha por instrução:

Volta dia no teste dia <= 5 Imprime dia no fim da volta
1 1 True Dia 1: ... 2
2 2 True Dia 2: ... 3
3 3 True Dia 3: ... 4
4 4 True Dia 4: ... 5
5 5 True Dia 5: ... 6
— 6 False — 6

Repare na última linha: o teste é feito uma vez a mais do que o corpo do laço. O laço deu cinco voltas, mas a condição foi avaliada seis vezes. É por isso que, ao fim, dia vale 6 e não 5 — e é a causa mais comum de erro de um a mais ou um a menos.

Onde o <= importa

while dia <= 5 dá cinco voltas. while dia < 5 dá quatro. Não é preciosismo: é uma noite inteira de laboratório procurando o dia que sumiu.

O acumulador

Contar voltas é útil. Juntar valores ao longo das voltas é o que faz o laço valer a pena.

# ACUMULADOR: uma variavel que guarda o que ja foi somado.
total = 0
numero = 1

while numero <= 10:
    total = total + numero
    numero = numero + 1

print("Soma de 1 ate 10:", total)
Soma de 1 ate 10: 55

O padrão se chama acumulador e tem sempre esta forma:

  1. uma variável começa em zero antes do laço;
  2. dentro do laço ela recebe variavel = variavel + alguma_coisa;
  3. depois do laço ela vale o total.

Rastreie:

Volta total no início numero total no fim
1 0 1 1
2 1 2 3
3 3 3 6
4 6 4 10
5 10 5 15

...e assim até numero chegar a 11, quando o teste falha e o total é 55.

💡 Pense assim: o carrinho do supermercado

O carrinho chega vazio — é o total = 0. Cada produto que entra soma ao que já estava lá. No caixa, o carrinho tem a compra inteira.

Se você despejasse o carrinho a cada corredor, chegaria ao caixa com o último produto e nada mais. Guarde essa imagem: ela é o erro da próxima seção.

Contador e acumulador no mesmo laço

Eles convivem sem problema, e quase sempre é isso que se quer:

# Contador e acumulador no mesmo laco: em que dia a franquia estoura.
franquia_gb = 20

usado_gb = 0
dias = 0

while usado_gb <= franquia_gb:
    consumo = float(input(f"Consumo do dia {dias + 1} em GB: "))
    usado_gb = usado_gb + consumo
    dias = dias + 1

print(f"A franquia estourou no dia {dias}")
print(f"Consumo acumulado: {usado_gb:.1f} GB de {franquia_gb} GB")
Consumo do dia 1 em GB: 1.4
Consumo do dia 2 em GB: 3.2
Consumo do dia 3 em GB: 0.8
Consumo do dia 4 em GB: 5.5
Consumo do dia 5 em GB: 2.1
Consumo do dia 6 em GB: 4.0
Consumo do dia 7 em GB: 3.9
A franquia estourou no dia 7
Consumo acumulado: 20.9 GB de 20 GB

É aqui que o laço mostra a que veio. Se o cliente gastasse sempre 1,4 GB por dia, nem precisaria de laço: 20 // 1.4 responde 14 dias numa linha só. Mas o consumo muda todo dia, e na hora de escrever o programa ninguém sabe quantos dias vão caber na franquia — podem ser sete, podem ser trinta. Não há conta a fazer de antemão: o jeito é ir somando dia a dia e perguntar, a cada volta, se a franquia já estourou.

Repare também em qual dia sai na resposta. O teste acontece antes de ler o consumo, então o dia que passa dos 20 GB é lido e somado, e só depois a condição fica falsa. Por isso o programa diz "estourou no dia 7": é exatamente o dia em que a operadora precisa avisar o cliente.

O erro que não dá erro, versão laço

Este é o momento central da unidade. Os dois laços abaixo são idênticos, linha por linha, a não ser pelo lugar onde o acumulador é criado:

# O MESMO laco, com o acumulador criado no lugar errado.
total_certo = 0
numero = 1
while numero <= 5:
    total_certo = total_certo + numero
    numero = numero + 1
print("Certo: ", total_certo)

numero = 1
while numero <= 5:
    total_errado = 0          # zerado a cada volta
    total_errado = total_errado + numero
    numero = numero + 1
print("Errado:", total_errado)
Certo:  15
Errado: 5

1 + 2 + 3 + 4 + 5 dá 15. O segundo laço imprimiu 5.

E não deu erro nenhum. Nenhum aviso, nenhum traceback, nenhuma linha vermelha. Ele rodou até o fim, imprimiu um número inteiro perfeitamente plausível e seguiu a vida.

O motivo aparece na hora quando se rastreia:

Volta total_errado no início zera para soma total_errado no fim
1 — 0 1 1
2 1 0 2 2
3 2 0 3 3
4 3 0 4 4
5 4 0 5 5

A linha total_errado = 0 está dentro do laço. Ela não roda uma vez: roda cinco vezes, jogando fora tudo o que já havia sido somado. No fim, a variável não guarda a soma — guarda o último valor, e só.

Por que este é o pior defeito possível

É o mesmo modo de falha da Lista 03 (input sem converter) e da Lista 04 (precedência), e agora com um agravante: o resultado errado é plausível. Uma soma de cinco parcelas dando 5 não choca ninguém que não conheça as parcelas.

Com trinta dias de consumo, o programa diria que o cliente gastou 1,2 GB no mês — o valor do último dia. Ninguém trava, ninguém reclama, e a conta vai errada para o cliente.

A defesa é uma pergunta, sempre a mesma: este número faz sentido? Some dois ou três valores de cabeça e compare. Um computador nunca vai te avisar disso.

Regra prática: o acumulador é criado onde ele tem de valer zero uma única vez — antes do while, na margem, sem indentação.

A sentinela: parar quando a pessoa mandar

Na franquia, quem decidia a hora de parar era a conta. Quando quem decide é a pessoa que está usando o programa, combina-se um valor que significa acabou. Esse valor se chama sentinela.

# SENTINELA: o laco para quando a pessoa digita o valor combinado.
total = 0
dias = 0

consumo = float(input("Consumo do dia em GB (0 para encerrar): "))

while consumo != 0:
    total = total + consumo
    dias = dias + 1
    consumo = float(input("Consumo do dia em GB (0 para encerrar): "))

print(f"Dias informados: {dias}")
print(f"Consumo total: {total:.1f} GB")
Consumo do dia em GB (0 para encerrar): Consumo do dia em GB (0 para encerrar): Consumo do dia em GB (0 para encerrar): Consumo do dia em GB (0 para encerrar): Consumo do dia em GB (0 para encerrar): Dias informados: 4
Consumo total: 7.1 GB

A saída acima é a saída padrão crua: como no capítulo 3, o Python não reimprime o que você digita, então os convites ficam grudados. Na tela do OnlineGDB a mesma execução aparece assim:

Consumo do dia em GB (0 para encerrar): 1.4
Consumo do dia em GB (0 para encerrar): 2.0
Consumo do dia em GB (0 para encerrar): 0.6
Consumo do dia em GB (0 para encerrar): 3.1
Consumo do dia em GB (0 para encerrar): 0
Dias informados: 4
Consumo total: 7.1 GB

Note o detalhe que confunde todo mundo: há dois input() no programa, e isso não é descuido.

  • o primeiro, antes do laço, existe para a condição ter o que testar na primeira volta;
  • o segundo, no fim do bloco, é o "avanço" — sem ele, a condição testaria para sempre o mesmo valor.

💡 Pense assim: a catraca do ônibus

A catraca pergunta uma vez por pessoa. Alguém tem de estar na frente dela antes da primeira pergunta — é o input() de fora. E, depois de cada passagem, a próxima pessoa precisa chegar — é o input() de dentro.

E repare: o valor 0 não entrou na conta. Quando ele é lido, a condição falha e o bloco não roda. A sentinela é a senha para sair, não um dado.

O laço infinito

É o erro mais barulhento do capítulo, e o único que se percebe na hora: o programa não termina.

Há exatamente duas causas.

1. Falta o avanço. A variável do teste nunca muda:

dia = 1
while dia <= 5:
    print(f"Dia {dia}: verifique seu consumo")

dia vale 1 para sempre, 1 <= 5 é eterno, e a tela enche de "Dia 1". Faltou o dia = dia + 1.

2. O avanço vai para o lado errado. A variável muda, mas se afasta da condição de parada:

saldo_gb = 20
while saldo_gb > 0:
    print(f"Restam {saldo_gb} GB")
    saldo_gb = saldo_gb + 1

Aqui o saldo cresce, e saldo_gb > 0 fica cada vez mais verdadeiro. Era - 1.

Uma variante da mesma causa é a sentinela que nunca chega: um laço que espera consumo != 0 mas cujo input() ficou fora do bloco lê o mesmo valor eternamente.

Como sair

No OnlineGDB, o botão vermelho Stop acima do terminal. Rodando no seu computador, Ctrl+C no terminal.

E o mais importante: não é um defeito do computador, e não perde o seu código. É o programa fazendo exatamente o que você escreveu. Pare, releia o bloco e procure a linha que deveria mudar a variável do teste.

Faça isto antes de rodar qualquer while

Ponha o dedo na condição e pergunte: qual linha, dentro do bloco, muda o valor que está sendo testado? Se você não achar essa linha, não rode — o laço é infinito.

Um erro que dá erro

Para variar, um que o Python denuncia na cara:

dia = 1
while dia <= 3:
print("Dia", dia)
dia = dia + 1
IndentationError: expected an indented block after 'while' statement on line 2

O while termina em dois-pontos e exige um bloco indentado, exatamente como o if. A mensagem até diz em que linha o while estava.

Erros comuns deste capítulo

Mensagem ou sintoma Causa provável
o programa não para e a tela enche falta o avanço, ou ele vai para o lado errado
o total é igual à última parcela o acumulador foi zerado dentro do laço
o total deu zero o acumulador está sendo somado fora do bloco (indentação)
deu uma volta a menos (ou a mais) < onde devia ser <=, ou vice-versa
IndentationError: expected an indented block after 'while' faltou indentar o corpo do laço
NameError na primeira volta a variável do teste não foi criada antes do while
a sentinela entrou na soma o valor está sendo somado antes de a condição ser testada
a sentinela nunca funciona falta o segundo input(), dentro do laço

Resumo

  • while condição: repete o bloco indentado enquanto a condição for verdadeira; o if pergunta uma vez, o while volta a perguntar.
  • Todo laço que termina tem três peças: início, teste e avanço.
  • A condição é testada uma vez a mais do que o corpo roda.
  • Acumulador: a variável nasce em zero antes do laço e cresce dentro dele. Criada dentro, ela entrega o último valor em vez do total — e não dá erro nenhum.
  • Sentinela: um valor combinado que encerra o laço. Precisa de um input() antes do laço e outro no fim do bloco.
  • Laço infinito é o programa obedecendo: ou falta o avanço, ou ele vai para o lado errado. Stop no OnlineGDB, Ctrl+C no terminal.
  • Rastreie no papel, uma linha por volta. É o que a prova cobra.

Para praticar

Lista 07 · Repetição com while. A Parte A é quase toda de tabelas de rastreio — faça-as no papel, sem computador, antes de abrir o OnlineGDB.