Você Não Precisa de range()
2020-04-16 #código #python #rangeQuem 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.