Quem está começando com Python tende a usar range() quando precisa iterar sobre listas. Mas isso não é realmente necessário.

Quando as pessoas começam a programar em Python, elas tendem a usar construções vindas de outras linguagens, e por isso iteram sobre uma lista da seguinte forma:

a_list = [1, 2, 3, 4]
for i in range(len(a_list)):
    print(a_list[i])

Mas Python tem o conceito de "iteráveis", o que quer dizer que algumas coisas podem ser iteradas diretamente, sem precisar acessar cada elemento individualmente. Por exemplo, nossa lista anterior poderia ser iterada com:

a_list = [1, 2, 3, 4]
for value in a_list:
    print(value)

"Para cada elemento em a_list, recupere-o e chame-o de value."

Vários elementos são iteráveis: Strings são iteráveis, retornando cada caractere nelas; dicionários são iteráveis, retornado cada chave neles; conjuntos são iteráveis, retornado cada elemento neles; tuplas são iteráveis, retornando cada elemento nelas; generators são iteráveis, retornando o próximo valor que eles conseguem produzir.

Mas e se precisássemos iterar sobre mais de um elemento ao mesmo tempo?

Entra o zip()

É aí que o zip() entra. zip() permite que você junte dois iteráveis:

a_list = [1, 2, 3, 4]
a_tuple = ('a', 'b', 'c', 'd')
for mixed_tuple in zip(a_list, a_tuple):
    print(mixed_tuple)

Esse código imprime:

(1, 'a')
(2, 'b')
(3, 'c')
(4, 'd')

O que o zip() faz é criar uma tupla com o primeiro elemento do primeiro iterável e o primeiro elemento do segundo iterável; depois com o segundo elemento do primeiro iterável e o segundo elemento do segundo iterável; e assim por diante. Você pode colocar quantos iteráveis você quiser no zip() e ele ira produzir tuplas maiores em cada iteração.

Interlúdio: Destruturação

Uma das coisas legais de Python é "destruturação". Destruturação (de-estruturar ou mais como "quebrar uma estrutura") permite que elementos de um iterável sejam extraídos diretamente.

Por exemplo, se você tem uma tupla com dois elementos:

a_tuple = (1, 2)

... você provavelmente iria extrair cada um dos elementos com alguma coisa do tipo:

a = a_tuple[0]
b = a_tuple[1]

Mas com destruturação, você pode fazer isso numa única passada com:

(a, b) = a_tuple

Este código e o acima dele fazem exatamente a mesma coisa.

Mas porque destruturação é importante se estamos falando sobre iterar sobre elementos? Porque for também tem a capacidade de destruturar:

a_list = [1, 2, 3, 4]
a_tuple = ('b', 'c', 'd', 'f')
a_string = 'aeio'

for (a_number, lowercase_char, uppercase_char) in zip(a_list, a_tuple, a_string):
    print(a_number)
    print(lowercase_char)
    print(uppercase_char)
    print()

Lembra que eu falei que strings também eram iteráveis e cada iteração traz um caractere? É isso.

Mas o que acontece quando um dos iteráveis é menor que o outro?

a_short_list = [1, 2]
a_long_list [10, 20, 30, 40, 50, 60, 70, 80, 90]
for (small, big) in zip(a_short_list, a_long_list):
    print(small, big)

Esse código imprime:

1 10
2 20

zip() pára quando o menor iterável não tem mais elementos. Para consumir todos os elementos do iterável mais longo, você precisa de itertools.zip_longest().

itertools.zip_longest()

zip_longest(), parte do módulo itertools, irá percorrer os iteráveis até que nenhum deles tenha mais elementos. O que acontece com o menor deles é que os seus valores são substituídos por None. Usando nosso exemplo anterior:

import itertools

a_short_list = [1, 2]
a_long_list [10, 20, 30, 40, 50, 60, 70, 80, 90]
for (small, big) in itertools.zip_longest(a_short_list, a_long_list):
    print(small, big)

Isso irá imprimir:

1 10
2 20
None 30
None 40
None 50
None 60
None 70
None 80
None 90

Cuidado com generators

Uma coisa que você precisa ter cuidado quando estiver usando zip() ou zip_longest() são generators. Por que? Porque alguns deles não tem fim.

Vamos usar um exemplo: cycle(). cycle(), também parte do módulo itertools, é um generator que, quando for pedido um valor, retorna o próximo valor de um iterável mas, quando chegar ao fim deste, retorna pro começo. Por exemplo (e eu estou usando zip() apenas para nos mantermos no tópico, mas não é preciso usar zip() para usar cycle()):

a_list = [10, 20, 30, 40, 50, 60, 70, 80, 90]
for (bullet, value) in zip(cycle(['-', '*', '.']), a_list):
    print(bullet, value)

Este código produz:

- 10
* 20
. 30
- 40
* 50
. 60
- 70
* 80
. 90

O que acontece é que zip() pegou o primeiro elemento do primeiro iterável, nosso cycle(['-', '*', '.']), que tem como primeiro valor no seu iterável '-' e o segundo valor do segundo iterável, 10; na próxima iteração, o segundo valor de cycle() foi '*' e o segundo valor de a_list foi 20; na terceira iteração, cycle() retornou '.' e a_list 30; agora, na quarta iteração, foi pedido um valor ao cycle() e, como o seu iterável terminou, ele retorno o primeiro valor, retornando '-' de novo.

Certo?

Então qual o problema com generators?

Alguns generators -- como o cycle() acima -- não tem fim. Se você trocar zip() por zip_longest() no exemplo acima, você vai ver que o código não irá terminar. Não são todos os generators que produzem valores de forma infinita, e você pode usá-los sem problema.

Não é só zip_longest() que tem problemas. Você pode botar dois cycle()s num zip() e ele vai ficar gerando tuplas sem parar.

Certo, legal, mas e se eu precisar mostrar o índice também?

enumerate() ao resgate!

Então, nós falamos sobre usar dois iteráveis ao mesmo tempo, mas e se precisarmos da posição também? E se a nossa lista for uma lista de resultados ordenados e nós precisamos mostrar a posição em si?

De novo, você pode ficar tentado a usar range():

winners = ['first place', 'second place', 'third place', 'fourth place']
for pos in range(len(winners)):
    print(pos + 1, winners[pos].capitalize())

Isso irá imprimir:

1 First place
2 Second place
3 Third place
4 Fourth place

Uma das coisas que você pode tentar ser esperto é tentar misturar o seu novo conhecimento sobre zip() e fazer:

winners = ['first place', 'second place', 'third place', 'fourth place']
for (pos, name) in zip(range(len(winners)), winners):
    print(pos + 1, name.capitalize())

... que, pessoalmente, parece mais complexo do que a primeira opção. Mas Python tem outro generator chamado enumerate() que recebe um único iterável, mas produz tuplas com o índice e seu valor:

winners = ['first place', 'second place', 'third place', 'fourth place']
for (pos, name) in enumerate(winners):
    print(pos + 1, name.capitalize())

Melhor ainda, enumerate() tem uma opção para definir o valor inicial do primeiro elemento, e ao invés de usar pos + 1 no print(), nós podemos mudar o enumerate para enumerate(winners, start=1) e remover a adição no print().

Conclusão

Iteráveis são as grandes potências de Python, como você pode ter percebido com a lista de coisas que podem ser iteradas. Entendendo-os vai lhe ajudar a escrever código Python melhor e mais conciso, sem perda de significado.


Esse conteúdo foi criado baseado nas discussões no Telegram do PyTche. Se quiser, junte-se a nós para conversarmos sobre Python.