Dev (Back & Front)ARTIGO

Explicando comprehensions para programadores

No primeiro ou no segundo ano programando com Python, eu nunca usei list comprehension (na época, eram comprehensions apenas). Eu li sobre elas, meio que descobri como trabalhavam, e depois agarrei map() e filter(), os quais eu entendi. Olhando para trás, acho que isso tem muito a ver com o fato de que as explicações de comprehensions são feitas usando a sua origem – a matemática – em vez do domínio em que as usamos – programação.

Uma busca rápida no DuckDuckGo me diz que esse ainda é o caso – a Wikipedia nos pede para considerar algo como isto: S = {2 ? x | x?N | x> 3}, e outras fontes também parecem começar com ‘isso é como é feito em matemática, então … ‘ (uma exceção notável é o tutorial sobre python.org).

Ao falar com programadores, eu gostaria de explicar comprehensions de maneira diferente, porque nem todos eles têm um background em matemática. Para um programador, uma list comprehension é simplesmente um loop de for´s para constrir listas, usando uma notação mais declarativa do que um loop for usual. Para aqueles de nós que acostumados a map() e filter(), list comprehension são as duas coisas também.

Considere o seguinte:

def loop(my_list):
result = []
for x in my_list:
if x > 3:
result.append(x*2)
return result

Já escreveu alguma vez um código como esse? Esse é o código que afirma explicitamente que medidas devem ser tomadas para a construção de sua lista; mas você não precisa – em vez disso, você pode indicar o que você deseja:

def compr(my_list):
return [x*2 for x in my_list if x > 3]

Isso se traduz para me dar valor*2 para cada valor em my_list, mas apenas se esse valor for superior a três. Note também que essa expressão funciona com map (multiplipar por dois) e filter (usar apenas os valores que são menores de dois). O normal seria [acrescentar algo à lista for para cada valor em um iterável, opcionalmente apenas se uma condição if for True para aquele valor].

Comprehensions também funcionam aninhadas – considere este código simples, mas feio:

def create_matrix_loop(size, default):
new_matrix = []
for y in range(size):
row = []
for x in range(size):
row.append(default)
new_matrix.append(row)
return new_matrix

Exemplo de saída:

>create_matrix_loop(3, None)
[[None, None, None],
[None, None, None],
[None, None, None]]

Uma vez que comprehensions podem ser aninhadas, isso pode ser substituído por:

def create_matrix_compr(size, default):
return [[default for x in range(size)] for x in range(size)]

Como um bônus adicional, quando não dizemos ao compilador como queremos fazer algo, mas sim o que nós queremos que seja feito, pode gerar um melhor – mais rápido – bytecode para nós. A versão loop de create_matrix está traduzida em 35 instruções de bytecode, e a versão usando uma list comprehension é apenas 20 (tente import dis; dis.dis (func) para ver como func aparece em bytecode) e, na realidade, muitas vezes você vai evitar fazer uma função ao usar comprehensions, uma vez que elas são concisas o suficiente, fazendo essa diferença ainda maior. Ao cronometrar as implementações, a diferença é evidente:

>timeit -n100 create_matrix_loop(1000, None)
100 loops, best of 3: 113 ms per loop
>timeit -n100 create_matrix_compr(1000, None)
100 loops, best of 3: 49.1 ms per loop

É isso mesmo: menos código, sintaxe declarativa, e uma execução mais rápida! (Nota: Eu usei o iPython ao criar e cronometrar os exemplos – é impressionante e você deve experimentá-lo)

?

Texto original disponível em http://blaag.haard.se/Explaining-comprehensions-to-programmers/

É programador, desenvolvedor, consultor e, ocasionalmente, professor e palestrante.

Ver perfil