Back-End

25 abr, 2012

Dicas, truques e hacks de Python – Parte 2

Publicidade

No artigo anterior, falamos de alguns truques para aproveitar o máximo de uma expressão com poucas linhas de código. Continuaremos agora falando sobre as listas inteligentes.

2 – Listas

2.1 – Compreensão de listas (list comprehensions)

Se você já usa o Python há muito tempo, você pelo menos já ouviu falar de compreensão de listas. São uma maneira de fazer caber um for loop, um if statement, e uma tarefa, tudo em uma linha. Em outras palavras, você pode mapear e filtrar a lista em uma única expressão.

2.1.1 – Mapeando a lista

Vamos começar com algo bem simples. Digamos que você está tentando elevar ao quadrado todos os elementos de uma lista. Um programador recém iniciado no Python provavelmente escreverá o código assim:

1numbers = [1,2,3,4,5]
2squares = []
3for number in numbers:
4 squares.append(number*number)
5# Now, squares should have [1,4,9,16,25]

Você eficientemente ‘mapeou’ uma lista para outra lista. Você também pode usar a map function, e fazer algo assim:

1numbers = [1,2,3,4,5]
2squares = map(lambda x: x*x, numbers)
3# Now, squares should have [1,4,9,16,25]

Esse código é definitivamente mais curto (uma linha em vez de três), mas é bem feio. É difícil dizer, com uma rápida olhada, o que a function map faz (ela aceita a função e a lista, e aplica a função a cada elemento daquela lista). Além disso, você tem que usar uma função que pareça meio bagunçada de alguma maneira. Se tivesse um outro jeito… talvez uma compreensão de lista:

1numbers = [1,2,3,4,5]
2squares = [number*number for number in numbers]
3# Now, squares should have [1,4,9,16,25]

Ela faz a mesma coisa que mostramos nos últimos dois exemplos, mas é mais curto (diferente do primeiro exemplo) e limpo (diferente do segundo exemplo). Ninguém terá problemas em determinar o que ela faz, mesmo se não conhecer Python. 

2.1.2 – Filtrando a lista

E se você estiver mais interessado em filtrar a lista? Digamos que você quer remover cada elemento com valor igual ou maior que 4? (ok, os exemplos não são muito reais, fazer o quê…). Um novato no Python escreveria:

1numbers = [1,2,3,4,5]
2numbers_under_4 = []
3for number in numbers:
4 if number < 4:
5 numbers_under_4.append(number)
6# Now, numbers_under_4 contains [1,4,9]

Bem simples, certo? Mas você precisou de 4 linhas, dois níveis de aninhamento, e um anexo para fazer algo completamente trivial. Você poderia reduzir o tamanho do código com a filter function:

1numbers = [1,2,3,4,5]
2numbers_under_4 = filter(lambda x: x < 4, numbers)
3# Now, numbers_under_4 contains [1,2,3]

Similar à map function da qual falamos acima, ela reduz o tamanho do código, mas está muito feio. O que está acontecendo? Como o map, o filter aceita a função e a lista. Ele avalia cada elemento da lista e, se eles forem avaliados como true, esse elemento é incluído na lista final. Claro que você também pode fazer isso com a compreensão de lista também:

1numbers = [1,2,3,4,5]
2numbers_under_4 = [number for number in numbers if number < 4]
3# Now, numbers_under_4 contains [1,2,3]

Mais uma vez, utilizar compreensão de lista nos dá um código mais curto, limpo e simples de entender.

2.1.3 – Map e Filter ao mesmo tempo

Agora entendemos o real poder das listas de compreensão. Se não consegui te convencer de que map e filter são uma grande perda de tempo até aqui, espero que isso resolva.

Digamos que quero usar o map e o filter em uma lista ao mesmo tempo. Em outras palavras, gostaria de ver elevado ao quadrado cada elemento da lista que estiver abaixo de 4. Mais uma vez, a maneira de um novato em Python:

1numbers = [1,2,3,4,5]
2squares = []
3for number in numbers:
4 if number < 4:
5 squares.append(number*number)
6# squares is now [1,4,9]

O código está começando a expandir na direção horizontal agora! O que poderíamos fazer para simplificar o código? Poderíamos tentar usar map e filter, mas eu não acho que será uma boa ideia…

1numbers = [1,2,3,4,5]
2squares = map(lambda x: x*x, filter(lambda x: x < 4, numbers))
3# squares is now [1,4,9]

Enquanto map e filter estavam feios antes, agora eles estão simplesmente ilegíveis. Obviamente essa não é uma boa ideia. Mais uma vez, a lista de compreensão salva o dia:

1numbers = [1,2,3,4,5]
2squares = [number*number for number in numbers if number < 4]
3# square is now [1,4,9]

Esse é um pouquinho mais longo que os outros exemplos de listas de compreensão, mas, na minha opinião, ainda é bem legível. É definitivametne melhor que usar for loop e que usar map e filter.

Como você pode ver, a lista de compreensão usa o filter e depois o map. Se você precisa usar o map, e depois o filter, as coisas ficam mais complicadas. Voce pode até ter que usar listas de compreensão aninhadas, os comandos map e filter, ou um for loop regular, dependendo do que é mais limpo. Essa discussão, no entanto, está fora do escopo deste artigo.

2.1.4   Generator Expressions

Existe um lado ruim das listas de compreensão: a lista inteira tem que ser armazenada de uma vez na memória. Isso não é um problema para listas pequenas, como as que utilizamos nos exemplos acima, nem para listas de maior magnitude. Mas, eventualmente, elas se tornam bastante ineficientes.

Generator expressions (gerador de expressões) são novos no Python 2.4, e possivelmente a coisa mais legal já publicada sobre Python. E eu acabei de descobri-las. Os geradores de expressões NÃO carregam a lista completa para a memória de uma vez, mas criam um ‘generator object’, de modo que somente um elemento da lista tenha que ser carregado de cada vez.

Claro que se você realmente precisa usar toda a lista para algo, isso não te ajuda muito. Mas se você está apenas a passando para algo que aceita qualquer objeto iterável – como um for loop – você pode usar a function generator.

O gerador de expressões tem a mesma sintaxe das listas de compreensão, mas usando parênteses em vez de colchetes:

1numbers = (1,2,3,4,5) # Since we're going for efficiency, I'm using a tuple instead of a list ;)
2squares_under_10 = (number*number for number in numbers if number*number < 10)
3# squares_under_10 is now a generator object, from which each successive value can be gotten by calling .next()
4
5for square in squares_under_10:
6 print square,
7# prints '1 4 9'

Isso é meramente mais eficiente do que usar uma lista de compreensão.

Portanto, você deve usar o gerador de expressões para um maior número de itens. Você sempre vai usar listas de compreensão se precisar de uma lista inteira de uma vez, por alguma razão. Se nenhuma dessas regras se aplica, use qualquer um deles. É uma boa prática usar gerador de expressões, a não ser que exista alguma razão para não usá-lo, mas você não vai ver nenhuma real diferença em eficiência, a não ser que a lista seja muito grande.

Como nota final, o gerador de expressões só precisa estar dentro de um conjunto de parênteses. Portanto, se você está chamando uma função unicamente com o gerador de expressões, só precisa de parênteses. Isso é válido no Phtyon: some_function(item for item in list).

2.1.5 – Usando ‘for’ para aninhar expressões

Listas de compreensão e geradores de expressões podem ser usados além de mapping e filtering; você pode criar listas bastante complexas com eles. Além de poder usar o map e filter, você pode aninhar (nest) as expressões for. Um novato no Python pode escrever algo como:

1for x in (0,1,2,3):
2 for y in (0,1,2,3):
3 if x < y:
4 print (x, y, x*y),
5
6# prints (0, 1, 0) (0, 2, 0) (0, 3, 0) (1, 2, 2) (1, 3, 3) (2, 3, 6)

Você pode ver que esse código é bem maluco. Com a lista de
compreensão, no entanto, você pode fazer isso de maneira mais rápida:

1print [(x, y, x * y) for x in (0,1,2,3) for y in (0,1,2,3) if x < y]
2# prints [(0, 1, 0), (0, 2, 0), (0, 3, 0), (1, 2, 2), (1, 3, 3), (2, 3, 6)]

Como você pode ver, esse código itera quatro valores de y, e para cada um desses valores, itera quatro valores de x, e então usa o filter e o map. Cada item da lista, então, é uma lista de x, y, x * y.

Note que o xrange(4) é um pouco mais limpo que (0,1,2,3), especialmente para listas mais longas, mas nós não chegamos lá ainda.

2.1.6 – Conclusão

Eu odeio dizer, mas nós apenas atingimos a superfície do que listas de compreensão e geradores de expressões podem fazer. Você realmente tem total poder de um for loop e de um if statement. Você pode fazer qualquer coisa (eu acho) que poderia fazer em qualquer um deles. Você pode operar em qualquer coisa que quer que comece como uma lista (ou qualquer outro iterável) e termine como uma lista (ou um gerador), incluindo listas de listas.

Uma lista de compreensão tem a sintaxe: [element for variable(s) in list if condition ]

Um gerador de expressões tem a sintaxe: (element for variable(s) in list if condition )

Lista Qualquer coisa que pode ser tratada como lista ou iterador
variáveis(eis) Variavél ou variáveis para atribuir o elemento da lista atual, da mesma maneira que um for loop
Condição Uma expressão inline do Python. O escopo mais uma vez inclui o escopo local e suas variáveis. Se isso se torna verdadeiro, o item será excluído do resultado
Elemento  

Uma expressão inline do Python. O escopo inclui o escopo local e suas variáveis. Este é o elemento real que será incluído no resultado

A(s) variável(eis) for na lista podem ser repetidas indefinidamente.

2.2 – Reduzindo uma lista

Infelizmente, você ainda não pode escrever uma programação inteira usando listas de compreensão (estou brincando… claro que você pode). Apesar de elas utilizarem map e filter, não existe uma maneira simples de usar listas de compreensão para reduzir uma lista. Com isso, quero dizer aplicar uma função para os dois primeiros elementos da lista, em seguida para aquele resultado e para o próximo elemento da lista, e assim em diante, até que um valor único é encontrado. Por exemplo, talvez você queira encontrar o produto de todos os valores na lista. Você poderia fazer um for loop:

1numbers = [1,2,3,4,5]
2result = 1
3for number in numbers:
4 result *= number
5# result is now 120

Ou você poderia usar a função reduce, que
aceita uma função que pegue dois argumentos, e uma lista:

1numbers = [1,2,3,4,5]
2result = reduce(lambda a,b: a*b, numbers)
3# result is now 120

Essa não é tão bonita quanto uma lista de compreensão, mas é menor que um for loop. Definitivamente vale a pena guardar.

2.3 – Iteração em vez de Lista: range, xrange e enumerate

Você se lembra (ou talvez não) de quando você programava em C, e for loops eram contados através de números indexados em vez de elementos? Você provavelmente já sabe como replicar esse comportamento no Python, usando range ou xrange. Ao passar o valor para range, você tem uma lista de contagem de números inteiros de 0 ao valor – 1, inclusive. Em outras palavras, ele te dá os valores do index de uma lista com aquele comprimento. O xrange faz a mesma coisa, apenas de maneira mais eficiente: ele não carrega a lista inteira na memória de uma só vez.

Aqui está um exemplo:

1strings = ['a', 'b', 'c', 'd', 'e']
2for index in xrange(len(strings)):
3 print index,
4# prints '0 1 2 3 4'

O problema aqui é que normalmente você acaba precisando dos elementos da lista de qualquer jeito. Qual o propósito de ter somente os valores do index? O Python tem uma função muito boa chamada enumerate que te dará os dois. Ao usar o enumerate em uma lista, ele vai retornar um iterador de index, com valores pares:

1strings = ['a', 'b', 'c', 'd', 'e']
2for index, string in enumerate(strings):
3 print index, string,
4# prints '0 a 1 b 2 c 3 d 4 e'

Outra vantagem do enumerate é que ele é consideravelmente mais limpo e legível que xrange(len()). Por causa disso, range e xrange são somente úteis quando você precisa criar uma lista de valores do zero, por alguma razão, em vez de usar aqueles de uma lista já existente.

2.4 – Checando a condição em todo e qualquer elemento de uma lista

Digamos que você quer checar para ver se qualquer elemento na lista satisfaz uma condição (digamos, abaixo de 10). Antes do Python 2.5, você poderia fazer algo do tipo:

1numbers = [1,10,100,1000,10000]
2if [number for number in numbers if number < 10]:
3 print 'At least one element is over 10'
4# Output: 'At least one element is over 10'

Se nenhum dos elementos satisfaz a condição, a lista de compreensão vai criar uma lista vazia, que é avaliada como falsa (false). Caso contrário, uma lista não-vazia será criada, e avaliada como verdadeira (true). Estritamente, você não precisa avaliar cada item na lista; você pode parar depois do primeiro elemento que satisfizer a condição. O método acima é menos eficiente, mas pode ser sua única escolha se você não puder usar unicamente o Python 2.5 e precisa espremer toda essa lógica em uma única expressão.

Com a nova função any introduzida no novo Python 2.5, você pode fazer a mesma coisa de maneira mais limpa e eficiente. O any é inteligente o suficiente para retornar True depois do primeiro item que satisfaz a condição. Aqui, eu usei um gerador de expressões que retorna um valor True ou False para cada elemento, e o passa para any. O gerador de expressões somente computa esses valores na medida em que eles são requisitados, e o any só pede o valor que ele precisa:

1numbers = [1,10,100,1000,10000]
2if any(number < 10 for number in numbers):
3 print 'Success'
4# Output: 'Success!'

Da mesma maneira, você pode chegar se todo elemento satisfaz a condição. Sem o Python 2.5, você teria que fazer algo assim:

1numbers = [1,2,3,4,5,6,7,8,9]
2if len(numbers) == len([number for number in numbers if number < 10]):
3 print 'Success!'
4# Output: 'Success!'

Aqui, usamos o filter com uma lista de compreensão, e checamos para ver se ainda temos todos aqueles elementos. Se sim, então todos os elementos satisfizeram a condição. Mais uma vez, isso é menos eficiente do que poderia ser, porque não existe necessidade de checar depois do primeiro elemento que não satisfaz a condição. E, mais uma vez, sem o Python 2.5, essa deve ser sua única opção para que toda a lógica caiba em uma única expressão.

Com o Python 2.5, existe uma maneira mais fácil: a função all function. Como você deve esperar, ela é inteligente o suficiente para parar depois do primeiro elemento que não combina, retornando False. Esse método funciona do mesmo modo que o método any descrito acima.

1numbers = [1,2,3,4,5,6,7,8,9]
2if all(number < 10 for number in numbers):
3 print 'Success!'
4# Output: 'Success!'

2.5 – Combinando listas múltiplas, item por item

A função zip pode ser usada para “fechar” listas juntas. Ela retorna uma lista de tuplas (tuples), na qual a enésima tupla contém o enésimo item para cada uma das listas passadas. Esse é um caso em que o exemplo é a melhor explicação:

1letters = ['a', 'b', 'c']
2numbers = [1, 2, 3]
3squares = [1, 4, 9]
4
5zipped_list = zip(letters, numbers, squares)
6# zipped_list contains [('a', 1, 1), ('b', 2, 4), ('c', 3, 9)]

Muitas vezes você vai usar esse tipo de coisa como um iterador para o for loop, retirando todos os três valores ao mesmo tempo (‘for letter, number, squares in zipped_list’).

2.6 – Alguns outros operadores de lista

A seguir, tem-se funções que podem ser chamadas em qualquer lista ou iterável:

  • max: Devolve o maior elemento da lista
  • min: Devolve o menor elemento da lista
  • sum: Devolve a soma de todos os elementos da lista. Aceita um segundo argumento opcional, o valor para começar quando se está somando (padrão 0).

2.7 – Lógica avançada com conjuntos

Eu admito que uma sessão sobre conjuntos não pertence a uma sessão sobre listas. Mas ao mesmo tempo que nunca uso conjuntos para muita coisa, eu ocasionalmente preciso fazer alguma lógica de conjuntos em alguma lista que tenho por aí. Os conjuntos se diferenciam de listas por imporem exclusividade (eles podem conter mais de um do mesmo item) e serem desordenados. Conjuntos também suportam várias operações lógicas diferentes.

A coisa mais comum que preciso fazer é garantir que minha lista é única. Isso é fácil: só tenho que convertê-la em um conjunto e checar se o comprimento é o mesmo:

1numbers = [1,2,3,3,4,1]
2set(numbers)
3# returns set([1,2,3,4])
4
5if len(numbers) == len(set(numbers)):
6 print 'List is unique!'
7# In this case, doesn't print anything

Claro que você pode converter o conjunto de volta em uma lista, mas lembre-se de que a ordem não é preservada. Para mais informações sobre as várias operações que os conjuntos suportam, dê uma olhada nos Python Docs. Você vai querer usar uma operação ou outra nas suas listas ou nos conjuntos no futuro.

No próximo artigo, falaremos sobre a construção e sobre manipulação de dicionários.

?

Texto original licenciado sob Creative Commons e disponível em http://www.siafoo.net/article/52