Neste post veremos outro exemplo da forma de repensarmos a iteração. Supondo que temos de percorrer uma estrutura bidimensional, como uma folha de cálculo. Uma forma de o fazer seria com dois ciclos um dentro do outro, o primeiro ciclo percorria as linhas e o segundo as colunas. As variáveis dos dois ciclos juntos seriam usadas para aceder ao conteúdo das células da folha de cálculo.

for linha in range(altura):
    for coluna in range(comprimento):
        valor = folha_de_calculo.obtem_valor(linha, coluna)
        calculo = fazer_algo(valor)
    
    if celula_prendida(calculo):
        break

Existe um problema com esta solução. Supondo que procuramos uma célula com determinada condição e encontrada essa condição, queremos terminar com a procura, não existe uma forma elegante de terminar dois ciclos de uma só vez. A expressão break do código acima apenas termina com o ciclo que percorre as colunas enquanto que o ciclo que percorre as linhas continua em execução a partir do inicio da próxima linha.

Claro que existem formas de escrever o código de maneira a terminar simultaneamente com ambos os ciclos, mas são formas confusas e desorganizadas.

def range_2d(comprimento, altura):
    #produz uma stream de 2 coordenadas
    for y in range(altura):
        for x in range(comprimento):
            yield x, y

for coluna, linha in range_2d(comprimento, altura):
    valor = folha_de_calculo.obtem_valor(linha, coluna)
    calculo = fazer_algo(valor)

    if celula_prendida(calculo):
        break

Uma solução mais interessante passa por alterar a iteração de forma a abstrair a sua natureza bidimensional. Criamos o generator range_2d() que recebe como argumentos a altura e o comprimento e percorre todas as coordenadas da matriz bidimensional para produzir um stream de pares (x, y) a representar as coordenadas. O generator faz isso utilizando o mesmo ciclo duplo encadeado que usamos na versão anterior, mas agora produz uma stream de pares que representa as coordenadas das células da folha de cálculo.

O nosso ciclo for usado para obter os valores da cada célula passou de um ciclo duplo encadeado para um ciclo simples. Cada iteração do generator range_2d() produz um novo par de coordenadas, processamos as coordenadas exactamente como fizemos na primeira versão do código, mas agora quando encontramos a célula pretendida podemos simplesmente terminar o ciclo, e seguir em frente.

Desta forma alteramos um ciclo duplo encadeado para um ciclo simples repensando sobre o que estávamos a iterar. Esta é uma melhor iteração porque encaixa melhor na nossa forma de descrevermos a iteração. Ninguém iria descrever a pesquisa de uma célula numa folha de cálculo dizendo "por cada linha da folha de cálculo, obter o valor de cada célula da linha", mas diria antes "obter o valor de cada célula na folha de cálculo", que é o que este ciclo for diz.

Este generator permite-nos descrever o processo de "obter o valor de cada célula na folha de cálculo" de uma maneira mais abstracta, permitindo que o programa execute o seu trabalho de forma mais expressiva e tornando o código mais fácil de ler, indo de encontro à nossa forma de pensar.

for celula in folha_de_calculo.celulas():
    valor = celula.obtem_valor()
    calculo = fazer_algo(valor)

    if celula_prendida(calculo):
        break


O melhor seria a folha de cálculo ter um método celulas() que providenciasse um iterador sobre as células da folha de cálculo. De reparar que o generator range_2d() socorre-se de inteiros para obter as células. Em vez de gerar inteiros seria muito melhor ter métodos para iterar directamente sobre os valores em que estamos interessados.


Ainda vamos continuar com esta temática durante mais dois ou três posts. Até já :)


Este post faz parte da série de posts sobre Ciclos e Interáveis em Python:
  1. Ciclos em Python, o básico
  2. Ciclos em Python e os Iteráveis
  3. Ciclos em Python, mais exemplos de Iteráveis
  4. Ciclos em Python, uso de Iteráveis fora dos ciclos
  5. Ciclos em Python, problemas comuns e os índices
  6. Ciclos em Python, iterar sobre duas listas
  7. Ciclos em Python, iteração personalizada
  8. Ciclos em Python, Generators - parte 1
  9. Ciclos em Python, Generators - parte 2
  10. Ciclos em Python, Generators - parte 3 (post actual)
  11. Ciclos em Python, operações de baixo nível
  12. Ciclos em Python, como tornar os nossos objectos em Iteráveis
  13. Ciclos em Python, conclusão


A inspiração para este post veio daqui.