Back-End

22 nov, 2016

Pare de escrever loops for em Python

Publicidade

“Flat is better than nested”, ou, no bom português, plano é melhor do que aninhado. Esse é um dos 19 aforismos do Zen of Python, um guia de melhores práticas escrito em 1999 por Tim Peters. Aforismo, esse, que pode ser facilmente ignorado quando lidamos com loops for no código, na tentativa de iterar sobre uma sequência para extrair uma informação ou gerar uma nova sequência a partir da original. Como, por exemplo, no código abaixo:

 

new_list = []
for tuple in iterable:
	for list in tuple:
		if len(list) > 3:
			for item in list:
				new_list.append(item)

São muitas camadas de código, muitos níveis de indentação, que quebram o tal do “Flat is better than nested”. A seguir, veremos alguns métodos do Python que tornam a escrita de uma iteração muito mais simples do que várias camadas de loops for.

Compreensão de lista

Essa é uma maneira concisa de substituir loops aninhados. Em sua estrutura, temos:

  • uma sequência de input,
  • uma variável que represente os membros daquela sequência,
  • uma expressão predicada opcional,
  • uma expressão de output que produza, a partir da sequência original, uma nova sequência com elementos da sequência de input que satisfaçam o predicado.

Sem compreensão de lista:

 

old_list = [2, 4, 7, 13, 16]
new_list = []
for element in old_list:
	if element % 2 == 0:
		new_list.append(element * 2)

Com compreensão de lista:

old_list = [2, 4, 7, 13, 16]
new_list = [element * 2 for element in old_list if element % 2 == 0]

Acima, temos a sequência de input (old_list), a variável para cada item da sequência (element), o predicado (if element % 2 == 0) e a expressão de output (element * 2). O resultado é o mesmo ([4, 8, 32]), mas, em vez de 4 linhas, usa-se apenas uma.

A compreensão de lista, como o nome diz, retorna uma lista completa no fim da operação. Mas existe uma maneira genérica, de alto desempenho e bastante eficiente, para os casos em que você não precisa usar sua memória para ter nova lista.

Expressão geradora

Se você remover os colchetes de uma compreensão de lista ([]) e envolver a expressão entre eles em parênteses (()), você obtém uma expressão geradora, que retorna um objeto único passível de iteração.

A estrutura é praticamente igual à de uma compreensão de lista:

new_list = [element * 2 for element in old_list if element % 2 == 0]
gen = (element * 2 for element in old_list if element % 2 == 0)
# <generator object <genexpr> at 0x101f9ea50>

Tanto a expressão geradora quanto a compreensão de lista permitem a inclusão de várias camadas de loops for e ifs:

long_list = [ [ [1, 2], [3, 4, 5], [6, 7, 8] ], [ [9, 10, 11], [12, 13], [14, 15] ] ]
print [item for list in long_list for sub_list in list if len(sub_list) < 3 for element in sub_list if element % 2 == 0]
# [2, 12, 14]

No exemplo acima, navegamos pelas três camadas de listas e ainda usamos algumas condicionais para criar uma nova lista apenas com os números pares. É ótimo ver todo um bloco de código aninhado reduzido a poucas linhas? Sim. É recomendado? Depende.

Quanto mais longa for sua compreensão de lista ou expressão geradora, mais difícil vai ser lê-la. E legibilidade é outro aforismo do Zen of Python: “Readability counts”. Talvez as alternativas abaixo possam ser melhor aproveitadas.

Funções

As funções map(), filter() também podem substituir os loops for, assim como a reduce(). A primeira aplica uma função a todos os elementos de uma sequência e retorna uma lista com os resultados.

Escreva:

 

long_list = [ [156, 24], [33, -240, 99], [6, 73, 58] ]
new_list = map(max, (list for list in long_list))
# [156, 99, 73]

Em vez de:

long_list = [ [156, 24], [33, -240, 99], [6, 73, 58] ]
new_list = []
for list in long_list:
	new_list.append(max(list)) 
# [156, 99, 73]

filter(), por outro lado, cria uma nova lista a partir dos elementos de uma sequência que façam uma função específica retornar true. Ótimo para condicionais dentro de loops:

long_list = [156, 24, 33, -240, 99, 6, 73, 58]
print filter(lambda x: x % 2 == 0, long_list)
# [156, 24, -240, 6, 58]

Por fim, reduce() reduz uma sequência a um único valor aplicando uma função de dois parâmetros consecutivamente aos elementos dela:

long_list = [156, 24, 33, -240, 99, 6, 73, 58]
print reduce(lambda x, y: x - 2 * y, long_list)
# 50

Vale lembrar também que muitas outras funções nativas do Python consomem iteráveis, então as opções são variadas.