11 de mar de 2012

List Comprehensions em Python

List comprehensions, em ciência da computação, é um construtor de listas que se baseia em outras listas pré-existentes.

Esse conceito não é novo e data dos anos 60's. Mas o termo comprehension foi usado pela primeira vez na linguagem NPL.

E as List Comprehension tem como origem as representação do conjuntos da matemática:

{x ∈ N | 0 < x < 7}
Lê-se: x pertence ao conjunto dos Naturais, tal que 0 é menor que x e x menor que 7. O que restringiria esse conjunto a {1, 2, 3, 4, 5, 6}.

Em Python, uma List comprehension tem a seguinte sintaxe:

[x * 2 for x in lista]
Nesse exemplo abaixo, multiplicamos os itens da lista por 2, atribuimos à variável nova_lista:

>>> lista = [1, 2, 3, 4, 5]  
>>> nova_lista = [x * 2 for x in lista]  
>>> nova_lista  
[2, 4, 6, 8, 10]  
Sendo que o 'x * 2' é executado em cada item da lista. Um código mais limpo  e intuitivo do que uma versão usando a builtin function map.

>>> def vezes_2(n):
...     return n * 2
...     
... 
>>> map(vezes_2, range(6))
[0, 2, 4, 6, 8, 10]
Poderiamos fazer essa operação com uma lambda function.

>>> map(lambda x: x * 2, range(6))
[0, 2, 4, 6, 8, 10]

Um código análogo sem List comprehension:

>>> lista = [1, 2, 3, 4, 5]  
>>> nova_lista = []  
>>> for x in lista:  
...   nova_lista.append(x * 2)  
...    
...  
>>>  
>>> nova_lista  
[2, 4, 6, 8, 10]  
Dentro das  List comprehensions ainda se pode usar condicionais, para filtrar elementos na criação das novas listas.

>>> frase = ['o', 'rato', 'roeu', 'a', 'roupa', 'do', 'rei', 'de', 'roma']
>>> [p for p in frase if p[0] == 'r'] 
['rato', 'roeu', 'roupa', 'rei', 'roma']
Aqui eu verifico se a primeira letra da palavra é 'r'.

Criei um pequeno script para comparar a performance das List comprehensions. Escrevi duas funções, uma usando List comprehensions e outra não, que criam uma lista com os números pares entre 0 e 1000000. Para fazer a comparação usei a biblioteca cProfile.

def foo():  
    l = []  
    for i in range(1000000):  
        if i % 2 == 0:  
            l.append(i)

def foo_listcomp():  
    l = [i for i in range(1000000) if i % 2 == 0]  

cProfile.run('foo()')  
cProfile.run('foo_listcomp()')  
Executando o código.

$ python bench.py  
   500004 function calls in 1.739 seconds  


Ordered by: standard name  

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.009    0.009    1.739    1.739 <string>:1(<module>)
     1    0.936    0.936    1.730    1.730 bench.py:61(foo)
500000    0.769    0.000    0.769    0.000 {method 'append' of 'list'...
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof...
     1    0.025    0.025    0.025    0.025 {range}
  

   4 function calls in 0.165 seconds  

Ordered by: standard name  

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.015    0.015    0.165    0.165 :1()
     1    0.125    0.125    0.150    0.150 bench.py:67(foo_listcomp)
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof...
     1    0.024    0.024    0.024    0.024 {range}  
O código convencional chega a ser 10 vezes mais lento que o código que utiliza List comprehension. 1.739 segundos contra 0.165.

A performance e a simplicidade do código justifica o uso das List comprehensions quando se quer criar listas a partir de listas, sua finalidade semântica. Mas quando a lógica envolvida na geração da nova lista é muito complexa, o seu uso pode diminuir a legibilidade do código drasticamente.

Referências:
Google Style Guide
Documentação Python
The History of Python

Nenhum comentário:

Postar um comentário