compreensão da lista vs. filtro lambda +

858

Por acaso, encontrei uma necessidade básica de filtragem: tenho uma lista e tenho que filtrá-la por um atributo dos itens.

Meu código ficou assim:

my_list = [x for x in my_list if x.attribute == value]

Mas então pensei: não seria melhor escrever assim?

my_list = filter(lambda x: x.attribute == value, my_list)

É mais legível e, se necessário para o desempenho, o lambda pode ser retirado para obter algo.

A pergunta é: existem algumas ressalvas no uso da segunda maneira? Alguma diferença de desempenho? Estou sentindo falta do Pythonic Way ™ inteiramente e devo fazê-lo de outra maneira (como usar o itemgetter em vez do lambda)?

Agos
fonte
19
Um exemplo melhor seria um caso em que você já tivesse uma função bem nomeada para usar como seu predicado. Nesse caso, acho que muito mais pessoas concordariam que filterera mais legível. Quando você tem uma expressão simples que pode ser usada como está em um listcomp, mas precisa ser empacotada em um lambda (ou similarmente construído com partialou operatorfunções, etc.) para passar filter, é quando os listcomps vencem.
abarnert
3
Deve-se dizer que, pelo menos em Python3, o retorno de filteré um objeto gerador de filtro e não uma lista.
Matteo Ferla

Respostas:

588

É estranho o quanto a beleza varia para pessoas diferentes. Acho a compreensão da lista muito mais clara que filter+ lambda, mas use o que achar mais fácil.

Há duas coisas que podem retardar o seu uso filter.

A primeira é a sobrecarga da chamada de função: assim que você usar uma função Python (seja criada por defou lambda), é provável que o filtro seja mais lento que a compreensão da lista. Quase certamente não é o suficiente para importar, e você não deve pensar muito em desempenho até ter cronometrado seu código e considerado um gargalo, mas a diferença estará lá.

A outra sobrecarga que pode ser aplicada é que o lambda está sendo forçado a acessar uma variável com escopo definido ( value). Isso é mais lento que acessar uma variável local e no Python 2.x a compreensão da lista acessa apenas variáveis ​​locais. Se você estiver usando o Python 3.x, a compreensão da lista será executada em uma função separada, portanto, ele também acessarávalue através de um fechamento e essa diferença não se aplicará.

A outra opção a considerar é usar um gerador em vez de uma compreensão da lista:

def filterbyvalue(seq, value):
   for el in seq:
       if el.attribute==value: yield el

Então, no seu código principal (que é onde a legibilidade realmente importa), você substituiu a compreensão da lista e o filtro por um nome de função que se espera ser significativo.

Duncan
fonte
68
+1 para o gerador. Tenho um link em casa para uma apresentação que mostra como geradores incríveis podem ser. Você também pode substituir a compreensão da lista por uma expressão geradora apenas mudando []para (). Além disso, concordo que a lista comp é mais bonita.
Wayne Werner
1
Na verdade, nenhum filtro é mais rápido. Basta executar alguns benchmarks rápidos usando algo como stackoverflow.com/questions/5998245/…
skqr 15/15
2
@skqr é melhor usar apenas o timeit para benchmarks, mas dê um exemplo em que você acha filtermais rápido o uso de uma função de retorno de chamada Python.
Duncan
8
@ tnq177 É a apresentação de David Beasley em geradores - dabeaz.com/generators
Wayne Werner
2
@ VictorSchröder sim, talvez eu não estivesse claro. O que eu estava tentando dizer era que no código principal você precisa ver a imagem maior. Na pequena função auxiliar, você só precisa se preocupar com essa função. O que mais está acontecendo lá fora pode ser ignorado.
Duncan
237

Esta é uma questão um pouco religiosa em Python. Embora Guidomapfilterreduce tenha considerado a remoção , e do Python 3 , houve uma reação suficiente que, no final, reducefoi movida apenas de built-ins para functools.reduce .

Pessoalmente, acho a compreensão da lista mais fácil de ler. É mais explícito o que está acontecendo com a expressão, [i for i in list if i.attribute == value]pois todo o comportamento está na superfície e não dentro da função de filtro.

Eu não me preocuparia muito com a diferença de desempenho entre as duas abordagens, pois é marginal. Eu realmente otimizaria isso apenas se isso provasse ser o gargalo no seu aplicativo, o que é improvável.

Também desde que o BDFL queria filtersair da linguagem, certamente isso automaticamente torna a compreensão da lista mais Pythonic ;-)

Tendayi Mawushe
fonte
1
Obrigado pelos links para a entrada de Guido, se nada mais para mim significa que vou tentar não usá-los mais, para não adquirir o hábito e não ser favorável à religião :)
traço
1
mas reduzir é o mais complexo a ser feito com ferramentas simples! mapa e filtro são triviais para substituir por compreensões!
Njzk2 30/05
8
não sabia que a redução foi rebaixada no Python3. obrigado pela compreensão! O reduzir () ainda é bastante útil na computação distribuída, como o PySpark. Eu acho que foi um erro ..
Tagar
1
@Tagar você ainda pode usar reduzir você só tem que importá-lo de functools
icc97
69

Como qualquer diferença de velocidade deve ser minúscula, o uso de filtros ou a compreensão de listas se resume a uma questão de gosto. Em geral, estou inclinado a usar compreensões (o que parece concordar com a maioria das outras respostas aqui), mas há um caso em que prefiro filter.

Um caso de uso muito frequente é extrair os valores de algum X iterável sujeito a um predicado P (x):

[x for x in X if P(x)]

mas às vezes você deseja aplicar alguma função aos valores primeiro:

[f(x) for x in X if P(f(x))]


Como exemplo específico, considere

primes_cubed = [x*x*x for x in range(1000) if prime(x)]

Eu acho que isso parece um pouco melhor do que usar filter. Mas agora considere

prime_cubes = [x*x*x for x in range(1000) if prime(x*x*x)]

Nesse caso, queremos filtercontra o valor pós-calculado. Além da questão de calcular o cubo duas vezes (imagine um cálculo mais caro), há a questão de escrever a expressão duas vezes, violando a estética DRY . Nesse caso, eu estaria apto a usar

prime_cubes = filter(prime, [x*x*x for x in range(1000)])
IJ Kennedy
fonte
7
Você não consideraria usar o prime por meio de outra compreensão da lista? Como[prime(i) for i in [x**3 for x in range(1000)]]
viki.omega9
20
x*x*xnão pode ser um número primo, como tem x^2e xcomo fator, o exemplo realmente não faz sentido de maneira matemática, mas talvez ainda seja útil. (Talvez pudéssemos encontrar algo melhor ainda?)
Zelphir Kaltstahl
3
Note que podemos usar uma expressão gerador de vez para o último exemplo, se não quer comer a memória:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
Mateen Ulhaq
4
@MateenUlhaq isso pode ser otimizado para prime_cubes = [1]salvar ambos os ciclos de memória e CPU ;-)
Dennis Krupenik
7
@DennisKrupenik Ou melhor,[]
Mateen Ulhaq
29

Embora filterpossa ser o "caminho mais rápido", o "caminho Pythonic" seria não se importar com essas coisas, a menos que o desempenho seja absolutamente crítico (nesse caso, você não usaria o Python!).

Umang
fonte
10
Comentários tardios a um argumento frequentemente visto: às vezes faz diferença ter uma análise executada em 5 horas em vez de 10, e se isso puder ser alcançado levando uma hora otimizando o código python, pode valer a pena (especialmente se houver confortável com python e não com linguagens mais rápidas).
bli
Mas o mais importante é o quanto o código-fonte nos deixa mais lentos tentando ler e entender!
thoni56
20

Eu pensei em acrescentar que, no python 3, filter () é realmente um objeto iterador, então você teria que passar sua chamada de método de filtro para list () para criar a lista filtrada. Então, no python 2:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = filter(lambda num: num % 2 == 0, lst_a)

as listas bec têm os mesmos valores e foram concluídas quase na mesma época em que filter () era equivalente [x para x em y se z]. No entanto, em 3, esse mesmo código deixaria a lista c contendo um objeto de filtro, não uma lista filtrada. Para produzir os mesmos valores em 3:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = list(filter(lambda num: num %2 == 0, lst_a))

O problema é que list () pega uma iterável como argumento e cria uma nova lista a partir desse argumento. O resultado é que o uso desse filtro no python 3 leva até duas vezes mais que o método [x for x in y if z] porque você precisa iterar sobre a saída de filter () e a lista original.

Jim50
fonte
13

Uma diferença importante é que a compreensão da lista retornará um listtempo enquanto o filtro retornar a filter, que você não pode manipular como a list(por exemplo: invocá len-lo, que não funciona com o retorno de filter).

Minha própria auto-aprendizagem me levou a uma questão semelhante.

Dito isto, se houver uma maneira de obter o resultado listde um filter, como você faria no .NET quando tiver lst.Where(i => i.something()).ToList(), estou curioso para saber.

EDIT: Este é o caso do Python 3, não 2 (veja a discussão nos comentários).

Adeynack
fonte
4
filter retorna uma lista e podemos usá-la. Pelo menos no meu Python 2.7.6.
thiruvenkadam
7
Não é o caso do Python 3. a = [1, 2, 3, 4, 5, 6, 7, 8] f = filter(lambda x: x % 2 == 0, a) lc = [i for i in a if i % 2 == 0] >>> type(f) <class 'filter'> >>> type(lc) <class 'list'>
Adeynack
3
"se existe uma maneira de ter a lista resultante ... estou curioso para conhecê-la". Basta ligar list()no resultado: list(filter(my_func, my_iterable)). E, claro, você pode substituir listpor set, ou tuple, ou qualquer outra coisa que exija uma iteração. Mas para qualquer pessoa que não seja programador funcional, o caso é ainda mais forte para usar uma compreensão de lista em vez de fazer filteruma conversão explícita para list.
Steve Jessop
10

Acho o segundo caminho mais legível. Ele diz exatamente qual é a intenção: filtrar a lista.
PS: não use 'list' como um nome de variável

incrédulo
fonte
7

geralmente filteré um pouco mais rápido se estiver usando uma função embutida.

Eu esperaria que a compreensão da lista fosse um pouco mais rápida no seu caso

John La Rooy
fonte
python -m timeit 'filter (lambda x: x em [1,2,3,4,5], intervalo (10000000))' 10 loops, o melhor de 3: 1,44 s por loop python -m timeit '[x for x no intervalo (10000000) se x em [1,2,3,4,5]] '10 loops, melhor de 3: 860 mseg por loop Na verdade, não ?!
giaosudau
@sepdau, as funções lambda não são incorporadas. Compreensões lista têm melhorado ao longo dos últimos 4 anos - agora a diferença é insignificante qualquer maneira, mesmo com funções internas
John La Rooy
7

Filtro é exatamente isso. Ele filtra os elementos de uma lista. Você pode ver que a definição menciona o mesmo (no link oficial dos documentos que mencionei antes). Visto que a compreensão da lista é algo que produz uma nova lista depois de agir sobre algo da lista anterior (a compreensão do filtro e da lista cria uma nova lista e não executa operações no lugar da lista antiga. Uma nova lista aqui é algo como uma lista com digamos, um tipo de dados totalmente novo. Como converter números inteiros em string etc.)

No seu exemplo, é melhor usar o filtro do que a compreensão da lista, conforme a definição. No entanto, se desejar, diga other_attribute a partir dos elementos da lista; no seu exemplo deve ser recuperado como uma nova lista, você poderá usar a compreensão da lista.

return [item.other_attribute for item in my_list if item.attribute==value]

É assim que me lembro sobre a compreensão de filtros e listas. Remova algumas coisas de uma lista e mantenha os outros elementos intactos, use o filtro. Use alguma lógica por conta própria nos elementos e crie uma lista diluída adequada para algum propósito, use a compreensão da lista.

thiruvenkadam
fonte
2
Ficarei feliz em saber o motivo da baixa da votação, para que não a repita novamente em nenhum outro lugar no futuro.
thiruvenkadam
a definição de filtro e compreensão de lista não era necessária, pois seu significado não estava sendo debatido. Que uma compreensão de lista deve ser usada apenas para listas “novas” é apresentada, mas não discutida.
Agos 02/02
Eu usei a definição para dizer que o filtro fornece uma lista com os mesmos elementos que são verdadeiros para um caso, mas com a compreensão da lista, podemos modificar os próprios elementos, como converter int para str. Mas ponto tomado :-)
thiruvenkadam 02/02
4

Aqui está um pequeno pedaço que eu uso quando preciso filtrar algo depois a compreensão da lista. Apenas uma combinação de filtro, lambda e listas (também conhecidas como a lealdade de um gato e a limpeza de um cachorro).

Nesse caso, estou lendo um arquivo, removendo linhas em branco, comentando linhas e qualquer coisa após um comentário em uma linha:

# Throw out blank lines and comments
with open('file.txt', 'r') as lines:        
    # From the inside out:
    #    [s.partition('#')[0].strip() for s in lines]... Throws out comments
    #   filter(lambda x: x!= '', [s.part... Filters out blank lines
    #  y for y in filter... Converts filter object to list
    file_contents = [y for y in filter(lambda x: x != '', [s.partition('#')[0].strip() for s in lines])]
rharder
fonte
Isso alcança muito em muito pouco código. Eu acho que pode ser um pouco demais de lógica em uma linha para entender facilmente e legibilidade é o que conta.
Zelphir Kaltstahl
Você poderia escrever isso comofile_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines)))
Steve Jessop
4

Além da resposta aceita, há uma caixa de canto quando você deve usar filtro em vez de uma compreensão de lista. Se a lista for removível, você não poderá processá-la diretamente com uma compreensão da lista. Um exemplo do mundo real é se você usa pyodbcpara ler resultados de um banco de dados. Os fetchAll()resultados cursorsão de uma lista unhashable. Nessa situação, para manipular diretamente os resultados retornados, o filtro deve ser usado:

cursor.execute("SELECT * FROM TABLE1;")
data_from_db = cursor.fetchall()
processed_data = filter(lambda s: 'abc' in s.field1 or s.StartTime >= start_date_time, data_from_db) 

Se você usar a compreensão da lista aqui, receberá o erro:

TypeError: unhashable tipo: 'list'

CWpraen
fonte
1
todas as listas são laváveis ​​em >>> hash(list()) # TypeError: unhashable type: 'list'segundo lugar isso funciona bem:processed_data = [s for s in data_from_db if 'abc' in s.field1 or s.StartTime >= start_date_time]
Thomas Grainger
"Se a lista é removível, você não pode processá-la diretamente com uma compreensão da lista." Isso não é verdade, e todas as listas são removíveis de qualquer maneira.
juanpa.arrivillaga
3

Levei algum tempo para me familiarizar com o higher order functions filtere map. Então, eu me acostumei com eles e gostei filter, pois era explícito que ele filtra mantendo o que é verdadeiro e me senti bem por conhecer alguns functional programmingtermos.

Então eu li esta passagem (Fluent Python Book):

As funções de mapa e filtro ainda estão embutidas no Python 3, mas desde a introdução de compreensões de lista e expressões de gerador, elas não são tão importantes. Um listcomp ou um genexp faz o trabalho de mapear e filtrar combinados, mas é mais legível.

E agora penso: por que se preocupar com o conceito de filter/ mapse você pode alcançá-lo com idiomas já amplamente difundidos, como compreensão de lista? Além disso mapse filterssão tipos de funções. Nesse caso, prefiro usar Anonymous functionslambdas.

Finalmente, apenas para testá-lo, cronometrei os dois métodos ( mape listComp) e não vi nenhuma diferença de velocidade relevante que justificasse argumentar sobre isso.

from timeit import Timer

timeMap = Timer(lambda: list(map(lambda x: x*x, range(10**7))))
print(timeMap.timeit(number=100))

timeListComp = Timer(lambda:[(lambda x: x*x) for x in range(10**7)])
print(timeListComp.timeit(number=100))

#Map:                 166.95695265199174
#List Comprehension   177.97208347299602
user1767754
fonte
0

Curiosamente, no Python 3, vejo o desempenho do filtro mais rápido que as compreensões de lista.

Eu sempre pensei que a compreensão da lista seria mais eficiente. Algo como: [nome para o nome em brand_names_db se o nome não for None] O bytecode gerado é um pouco melhor.

>>> def f1(seq):
...     return list(filter(None, seq))
>>> def f2(seq):
...     return [i for i in seq if i is not None]
>>> disassemble(f1.__code__)
2         0 LOAD_GLOBAL              0 (list)
          2 LOAD_GLOBAL              1 (filter)
          4 LOAD_CONST               0 (None)
          6 LOAD_FAST                0 (seq)
          8 CALL_FUNCTION            2
         10 CALL_FUNCTION            1
         12 RETURN_VALUE
>>> disassemble(f2.__code__)
2           0 LOAD_CONST               1 (<code object <listcomp> at 0x10cfcaa50, file "<stdin>", line 2>)
          2 LOAD_CONST               2 ('f2.<locals>.<listcomp>')
          4 MAKE_FUNCTION            0
          6 LOAD_FAST                0 (seq)
          8 GET_ITER
         10 CALL_FUNCTION            1
         12 RETURN_VALUE

Mas eles são realmente mais lentos:

   >>> timeit(stmt="f1(range(1000))", setup="from __main__ import f1,f2")
   21.177661532000116
   >>> timeit(stmt="f2(range(1000))", setup="from __main__ import f1,f2")
   42.233950221000214
Rod Senra
fonte
8
Comparação inválida . Primeiro, você não está passando uma função lambda para a versão do filtro, o que a torna padrão para a função de identidade. Ao definir if not Nonena compreensão da lista, você está definindo uma função lambda (observe a MAKE_FUNCTIONinstrução). Segundo, os resultados são diferentes, pois a versão de compreensão da lista removerá apenas o Nonevalor, enquanto a versão do filtro removerá todos os valores "falsos". Dito isto, todo o objetivo do microbenchmarking é inútil. São um milhão de iterações, vezes 1k itens! A diferença é insignificante .
Victor Schröder
-7

Minha vez

def filter_list(list, key, value, limit=None):
    return [i for i in list if i[key] == value][:limit]
tim
fonte
3
inunca foi dito ser um dict, e não há necessidade limit. Fora isso, como isso é diferente do que o OP sugeria e como ele responde à pergunta?