Ciclos for e ciclos while.
Os ciclos permitem ao programador descrever tarefas repetitivas duma forma prática. As instruções relacionadas com ciclos são as seguintes:
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:
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.
>>> n = 50 >>> for i in range(20,n,3): ... print(i) ... 20 23 26 29 32 35 38 41 44 47 |
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:
>>> n = 10 >>> for i in range(n): ... print(i) ... 0 1 2 3 4 5 6 7 8 9 |
>>> colors = ["vermelho", "amarelo", "azul", "vermelho"] >>> for i in colors: ... print(i) ... vermelho amarelo azul vermelho |
>>> colors = ("vermelho", "amarelo", "azul", "vermelho") >>> for i in colors: ... print(i) ... vermelho amarelo azul vermelho |
Um conjunto guarda os elementos sem repetição. Assim se explica o que está a ser escrito (o "vermelho" não aparece repetido):
>>> colors = {"vermelho", "amarelo", "azul", "vermelho"} >>> print(colors) {'vermelho', 'amarelo', 'azul'} >>> for c in colors: ... print(c) ... amarelo azul vermelho |
>>> s = "Python" >>> for i in s: ... print(i) ... P y t h o n |
>>> mounts = {"janeiro":31, "fevereiro":28, "março": 31} >>> for m in mounts: ... print(f"{m} -> {mounts[m]}") ... janeiro -> 31 fevereiro -> 28 março -> 31 |
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.
def print_permutations_2(m: int, n: int): """ Print all the permutations of i in range(m), j in range(n). """ for i in range(m): for j in range(n): print(f"({i}, {j})") def main(): print_permutations_2(4, 3) >>> main() (0, 0) (0, 1) (0, 2) (1, 0) (1, 1) (1, 2) (2, 0) (2, 1) (2, 2) (3, 0) (3, 1) (3, 2) |
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.
def average_of_range(a: int, b: int) -> float: """ Average of the values in a range of integers. Precondition: a <= b """ sum = 0 for i in range(a, b+1, 1): sum += i return sum / (b - a + 1) >>> average_of_range(0, 10) 5.0 >>> average_of_range(1, 10) 5.5 >>> average_of_range(10, 20) 15.0 >>> average_of_range(10, 10) 10.0 |
A função abaixo testa se uma dada string s contém um dado caráter c:
def search(s: str, c: str) -> bool: """ Check if the char c occurs in the string s Precondition: len(c) == 1 """ for i in s: if i == c: return True return False >>> search("Python", 't') True >>> search("Python", 'x') False |
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:
def search_wrong(s: str, c: str) -> bool: for i in s: if i == c: return True else: return False |
def search(s: str, c: str) -> bool: """ Check if the char c occurs in the string s Precondition: len(c) == 1 """ for i in s: if i == c: return True else: pass return False |
def search(s: str, c: str) -> bool: """ Check if the char c occurs in the string s Precondition: len(c) == 1 """ found = False for v in s: if v == c: found = True return found |
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.
def search(s: str, c: str) -> bool: """ Check if the char c occurs in the string s Precondition: len(c) == 1 """ found = False for v in s: if v == c: found = True break return found >>> search("Python", 't') True >>> search("Python", 'x') False |
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.
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:
import math def f(x: float) -> float: return x * x def table_square(a: float, b: float, step: float): """ Print table for the function f with x varying between a and b inclusive """ n = math.floor((b - a)/step) + 1 # número de pontos de avaliação for i in range(0,n,1): x = a + step * i y = f(x) print(f"f({x:0.6f}) = {y:0.6f}") >>> table_square(0.0, 2.1, 0.1) f(0.000000) = 0.000000 f(0.100000) = 0.010000 f(0.200000) = 0.040000 f(0.300000) = 0.090000 f(0.400000) = 0.160000 f(0.500000) = 0.250000 f(0.600000) = 0.360000 f(0.700000) = 0.490000 f(0.800000) = 0.640000 f(0.900000) = 0.810000 f(1.000000) = 1.000000 f(1.100000) = 1.210000 f(1.200000) = 1.440000 f(1.300000) = 1.690000 f(1.400000) = 1.960000 f(1.500000) = 2.250000 f(1.600000) = 2.560000 f(1.700000) = 2.890000 f(1.800000) = 3.240000 f(1.900000) = 3.610000 f(2.000000) = 4.000000 f(2.100000) = 4.410000 |
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:
inicialização while condição: corpo # inclui o "avanço"
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:
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:
def average_of_range(a: int, b: int) -> float: """ Average of the integers between a and b inclusive """ Precondition: a <= b """ sum = 0 i = a # inicialização while i <= b: # condição sum +=i # corpo i += 1 # corpo (avanço) return sum / (b - a + 1) |
Compare com a versão que usa um ciclo for.
def average_of_range(a: int, b: int) -> float: """ Average of the integers between a and b inclusive """ Precondition: a <= b """ sum = 0 for i in range(a, b+1, 1): sum += i return sum / (b - a + 1) |
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.
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:
def is_prime(n: int) -> bool: # está na teórica 6 pass def next_prime(n: int) -> int: while not is_prime(n): n += 1 # avanço return n >>> next_prime(5555653) # já é primo 5555653 >>> next_prime(5555654) # não é primo 5555677 |
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
def execute(while C: B, state): if evaluate(C, state): state1 = execute(B, state) state2 = execute(while C: B, state1) return state2 else: return state |
O que a definição diz é o seguinte:
Se perguntarmos, logo no início, a quantidade de valores a somar, então podemos usar um ciclo for:
def read_and_sum(n: int) -> int: """ Input and add a sequence of integers. """ sum = 0 for i in range(n): v = int(input("> ")) sum += v return sum def main(): n = int(input("Introduza a quantidade de números a somar: ")) print(read_and_sum(n)) main() >>> main() Introduza a quantidade de números a somar: 5 > 10 > 11 > 10 > 11 > 10 52 |
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:
END_MARK = -1 def read_and_sum() -> int: """ Input and add a sequence of integers. """ sum = 0 v = int(input("> ")) # leitura antes do ciclo while v != END_MARK: sum += v v = int(input("> ")) # leitura dentro do ciclo (avanço) return sum def main(): print(f"Introduza uma sequência de números para somar, terminada por {END_MARK}") print(read_and_sum()) main() >>> main() Introduza uma sequência de números para somar, terminada por -1 > 10 > 11 > 10 > 11 > 10 > -1 52 |
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:
END_MARK = -1 def read_and_sum() -> int: """ Input and add a sequence of integers. """ sum = 0 while True: v = int(input("> ")) # avanço if v == END_MARK: # condição dum ciclo com saída pelo meio break sum += v return sum def main(): print(f"Introduza uma sequência de números para somar, terminada por {END_MARK}") print(read_and_sum()) main() |
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.
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.
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.
Este tipo de ciclo tem a particularidade de executar pelo menos uma iteração, visto a condição ser testada no final.
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:
sum = 0 while i < size: sum += l[i] i += f(i) sum = 0 while True: if i >= size: # mau estilo, porque está a complicar e até a confundir break sum += l[i] i += f(i)