Introdução à Programação para a Ciência e Engenharia (2024/2025)

Aulas teóricas

Artur Miguel Dias



Teórica 08 (03/out/2024)

Ciclos for e ciclos while.



Ciclos

Os ciclos permitem ao programador descrever tarefas repetitivas duma forma prática. As instruções relacionadas com ciclos são as seguintes:

Cada repetição da tarefa repetitiva chama-se uma iteração. Por esse motivo as instruções que implementam ciclos são também conhecidas por instruções iterativas.

Na aula teórica 5 falámos pela primeira vez em instruções de controlo. As quatro instruções indicadas acima são todas de controlo.

Já vimos, na aula teórica 2, que em Python também se pode exprimir repetição usando funções recursivas. Mas quando se usa o Python para escrever programas no chamado estilo procedimental (que é o nosso objetivo, de momento), o uso de ciclos é o que está previsto.

Há dois tipos de ciclos em Python:

Usar um ou outro tipo de ciclo não é uma questão de preferência:



Ciclos for

O ciclo for do Python é muito flexível e prático porque permite iterar sobre os valores de qualquer contentor suportado pela linguagem.

Um contentor é um objeto que pode conter outros objetos: conjunto, lista, tuplo, range (intervalo), string, etc. Os contentores cujo conteúdo siga uma dada ordem, têm a designação adicional de sequências.

Vamos agora analisar muitos exemplos de utilização do for usando diversos tipos de contentores.

Nestes exemplos, vamos apresentar apenas situações que envolvem escrita de dados, porque assim conseguimos observar imediatamente os efeitos do que está a acontecer.

for aplicado a intervalo (range)

O seguinte exemplo escreve os números inteiros de 20 até 49, com um passo de 3: Eis detalhe prático sobre a construção range: Se a sequência de inteiros começar em 0,então pode omitir-se a indicação do início da sequência. Além disso, quando o passo é 1, pode omitir-se o passo. Por exemplo, range(n) é equivalente a range(0,n,1).

Usar ou não a forma abreviada nos ranges é uma questão de preferência pessoal.

O seguinte exemplo escreve os números inteiros de 0 até 9, com um passo de 1:

for aplicado a lista

O seguinte exemplo escreve o nome de algumas cores: O seguinte exemplo também escreve o nome de algumas cores:

for aplicado a conjunto

O seguinte exemplo escreve o nome de algumas cores, mas agora através dum conjunto.

Um conjunto guarda os elementos sem repetição. Assim se explica o que está a ser escrito (o "vermelho" não aparece repetido):

for aplicado a string

O seguinte exemplo escreve os carateres duma string. Faz sentido, porque uma string é uma sequência:

for aplicado a dicionário

Este exemplo só será completamente percebido quando dermos atenção aos dicionários, numa aula futura.

Ciclos for encaixados (imbricados)

O que acontece se escrevermos um ciclo for dentro de outro ciclo for?

Neste caso ocorre um efeito multiplicativo: para cada valor do ciclo exterior, o ciclo interior é executado.

O seguinte exemplo escreve todas as permutações de comprimento 2 que se conseguem obter combinando os primeiros m naturais e os primeiros n naturais.

A função é testada usando os valores 4 e 3. Repare que m * n = 4 * 3 = 12, sendo esse o número de linhas escritas.

Repare que são primeiro escritos todos os pares com 0 na esquerda; a seguir com 1 na esquerda, etc. Assim, confirma-se que para cada valor do ciclo exterior, o ciclo interior corre completamente.

Ciclos for que acumulam

Muitas das utilizações do ciclo for envolvem problemas de acumulação. Já vimos diversos problemas desta natureza nas aulas práticas, incluindo a função factorial.

O seguinte exemplo calcula a média dos valores inteiros entre a e b, com a<=b. A função soma os valores e divide pela quantidade de valores.

Neste exemplo, a variável sum costuma ser designada como variável de acumulação. Ela é inicializada com o elemento neutro da operação em causa. Depois, dentro do ciclo, acumula-se nela, a pouco e pouco, aquele que será o valor final.

Ciclo for que efetua uma pesquisa

Para programar uma pesquisa usando um ciclo for, vamos usar adicionalmente as instruções return ou break.

Exemplo de pesquisa usando return

A função abaixo testa se uma dada string s contém um dado caráter c:

O ciclo for começa com a intenção de percorrer a string completa:

Note que seria um grave erro escrever o código abaixo, que não faz o que se pretende:

Se você desejar mesmo explicitar o else correspondente ao if que está dentro do ciclo, não fica muito elegante, mas seria assim: Se você não quiser usar um return dentro do ciclo, isso é possível com a ajuda duma variável auxiliar (found). A legibilidade é equivalente, mas perde-se eficiência, porque o ciclo vai sempre até ao final.

Exemplo de pesquisa usando break

Vamos resolver o mesmo problema de pesquisa de mais uma forma ainda: quando o caráter procurado é encontrado, quebra-se (termina-se) o ciclo for, mas desta vez sem abandonar a função corrente.

Para quebrar um ciclo, usa-se a instrução break, que faz a execução do for terminar imediatamente.

Tomando o exemplo anterior, agora acrescentamos um simples break que será executado quando o valor procurado for descoberto. A ineficiência que foi referida antes, ficou resolvida.

Nos nossos programas, quando quisermos interromper um ciclo a meio, é provável que usemos mais a técnica do return do que a técnica do break. Será frequente implementarmos um ciclo numa função independente, como fizemos no caso da pesquisa.

Ciclo for para intervalo de reais?

Os intervalos (ranges) do Python só suportam números inteiros. Seria interessante poder escrever expressões como range(0.0, 2.1, 0.1), mas tal não é suportado em Python.

No entanto, não é difícil fazer uma redução a intervalos de inteiros. Para começar, é possível escrever um intervalo de inteiros que faça o mesmo número de iterações que faria o indisponível intervalo de reais.

A função abaixo, escreve uma tabela para a função f(x) = x2 num dado intervalo real [a, b], com um dado passo step. Observe os detalhes associados às variáveis x e y:



Ciclos while

O ciclo while é mais geral do que o for e permite tratar qualquer situação que necessite dum ciclo. Recorde que o for só serve para iterar sobre contentores.

A instrução while é necessária quando a repetição de dada ação depende duma condição lógica requerida pelo problema: por exemplo, podemos querer repetir algo enquanto o valor duma variável for diferente de zero.

Na prática, um ciclo while é praticamente sempre usado de acordo com o seguinte esquema:

O significado as linhas anteriores é o seguinte: Após a inicialização, um ciclo while executa uma sequência de instruções (o corpo do while) enquanto uma dada condição (a condição do while) for verdadeira.

O corpo do while tem de especificar dois aspetos:

Mais aspetos essenciais do while:

Exemplo introdutório com while

Vamos considerar novamente o exemplo do cálculo da média dos valores inteiros entre a e b, com a<=b.

Seria mais simples programar usando um for, mas aqui vamos optar por um ciclo while, para ver como fica:

Entenda o código anterior e analise bem: a inicialização, a condição, o corpo, o avanço.

Comparando os ciclos for com os ciclos while

Algo que chama logo a atenção no código anterior é que, quando usamos um while, temos de ser nós a gerir a variável do ciclo i, algo que um ciclo for faz automaticamente.

Compare com a versão que usa um ciclo for.

Esta segunda versão é mais legível porque destaca logo na primeira linha do for os limites de variação.

Por outro lado, para perceber a versão com while, temos de procurar ativamente onde se encontra a inicialização e o avanço. Num ciclo while grande, a procura desses dois elementos pode demorar alguns segundos. Depois ainda temos de ver se os dois elementos fazem sentido e estão corretos.

Quando estiver em causa um contentor, preferimos usar um ciclo for. Quando não estiver em causa um contentor, usaremos um ciclo while sem qualquer hesitação.

Primeiro problema que exige um ciclo while

Considere o problema de procurar o primeiro número primo maior ou igual a um dado inteiro n.

Eis a nossa solução. O ciclo da função next_prime é rudimentar e evolui de 1 em 1 (a partir de n), testando se o valor corrente é primo:

Pergunta: Como é que podemos justificar que este ciclo termina sempre?

Resposta: A Matemática diz-nos que a quantidade de números primos é infinita e portanto a procura irá terminar mais cedo ou mais tarde

Perceber melhor os ciclos while

A seguinte definição matemática, que tenta usar a sintaxe do Python, descreve matematicamente o funcionamento dum ciclo while. É uma definição recursiva, como acontece normalmente na matemática: Para perceber esta definição, temos de começar por aprender o seguinte: Num programa, o conjunto das variáveis mais os respetivos valores, designa-se por estado do programa. Quando um programa é executado, o estado do programa vai mudando ao longo do tempo (usando a instrução de atribuição, ou de outras formas).

O que a definição diz é o seguinte:

Segundo problema que requer um ciclo while

Consideremos agora um novo problema: ler uma sequência de inteiros positivos a partir do teclado e calcular a soma desses inteiros.

Se perguntarmos, logo no início, a quantidade de valores a somar, então podemos usar um ciclo for:

Está feito!

Mas agora vamos assumir que não sabemos à partida o número de valores a somar. O utilizador introduzirá um valor convencional, por exemplo -1, para indicar o final da sequência.

Eis a primeira tentativa de solução:

Esta solução está correta e já é razoavelmente boa e é perfeitamente aceite em IPCE!

Mas há dois aspetos que complicam um pouco a legibilidade e a manutenção futura do programa:

Haverá forma de evitar a duplicação e mudar a ordem das instruções no corpo?

Eis uma solução "revolucionária". Consiste em fazer a leitura exclusivamente dentro do ciclo e usar um break para terminar o ciclo.

Estamos perante um exemplo de ciclo com saída pelo meio, onde a condição que determina o final do ciclo ocorre a meio:

Quem observa esta solução pela primeira vez, normalmente estranha a condição do ciclo ser True, mas a verdade é que não há nada que necessite de ser testado nesse ponto.

Esta solução pode dizer-se "revolucionária" porque ignora a forma normal de usar o while: a condição do while é trivializada (fica um simples True) e inventa-se uma condição no interior do corpo que acaba por ser a condição decisiva para a lógica do ciclo.

Este tipo de ciclos com saída pelo meio foram discutidos por Dijkstra nos anos 1960. Ele chamou a atenção para a conveniência de suportar este tipo de ciclos que designou de ciclos Loop-and-a-Half.


Ciclos com saída pelo início, pelo meio e pelo final

Saída pelo início

Como a condição do while é testada no início, costuma dizer-se que um ciclo while normal é um ciclo com saída pelo início.

Um ciclo for também é um ciclo com saída pelo início, porque o for testa uma condição interna antes de cada iteração, incluindo a primeira iteração.

Há muitos problemas em que usar um ciclo com saída pelo início é o que convém para a lógica da solução.

Como já foi dito, os ciclos com saída pelo início podem executar zero iterações.

O Python suporta diretamente ciclos com saída pelo início, através das instruções while e for.

Saída pelo meio

Se a lógica da solução convidar a testar a condição do ciclo a meio do corpo, podemos usar a técnica do exercício anterior. Ou então tentar reformular a solução noutros moldes (com algum risco de ficar mais complicado e menos legível).

Algumas linguagens de programação oferecem uma instrução específica para ciclos com saída pelo meio. Por exemplo, em Ada, a instrução loop/exit when.

Em Python, através da instrução break, conseguimos imitar este tipo de instruções de forma bastante aproximada. Podemos afirmar que o Python suporta ciclos com saída pelo meio através da instrução break.

Saída pelo final

Ao contrário do Python, muitas linguagens de programação dispõem dum tipo de ciclo com saída pelo final, por exemplo, repeat/until em Pascal e do/while em Java.

Este tipo de ciclo tem a particularidade de executar pelo menos uma iteração, visto a condição ser testada no final.


Discussão final sobre o uso de return ou break nos ciclos

Só se deve usar return ou break nos ciclos com boa justificação:

A maioria dos ciclos dispensa o seu uso. Não faz sentido usar essas instruções de forma injustificada.

Por exemplo, os dois ciclos abaixo são equivalentes. Mas o segundo é um exemplo de mau estilo, porque está a implementar um ciclo de forma desnecessariamente complicada, portanto menos legível:



#70