compréhension de la liste vs lambda + filtre


859

Je me suis trouvé avoir un besoin de filtrage de base: j'ai une liste et je dois la filtrer par un attribut des éléments.

Mon code ressemblait à ceci:

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

Mais alors j'ai pensé, ne serait-il pas préférable de l'écrire comme ça?

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

C'est plus lisible, et si nécessaire pour la performance, le lambda pourrait être retiré pour gagner quelque chose.

La question est: y a-t-il des réserves à utiliser la deuxième façon? Une différence de performance? Suis-je complètement absent du Pythonic Way ™ et dois-je le faire d'une autre manière (comme utiliser itemgetter au lieu du lambda)?


19
Un meilleur exemple serait un cas où vous aviez déjà une fonction bien nommée à utiliser comme prédicat. Dans ce cas, je pense que beaucoup plus de gens conviendraient que filterc'était plus lisible. Lorsque vous avez une expression simple qui peut être utilisée telle quelle dans un listcomp, mais doit être enveloppée dans un lambda (ou construit de manière similaire à partir de partialou des operatorfonctions, etc.) pour passer filter, c'est là que les listcomps gagnent.
abarnert

3
Il faut dire qu'en Python3 au moins, le retour de filterest un objet générateur de filtre et non une liste.
Matteo Ferla

Réponses:


589

Il est étrange combien la beauté varie selon les personnes. Je trouve la compréhension de la liste beaucoup plus claire que filter+ lambda, mais utilisez celle que vous trouvez plus facilement.

Il y a deux choses qui peuvent ralentir votre utilisation filter.

Le premier est la surcharge de l'appel de fonction: dès que vous utilisez une fonction Python (qu'elle soit créée par defou lambda), il est probable que le filtre soit plus lent que la compréhension de la liste. Ce n'est certainement pas suffisant pour avoir de l'importance, et vous ne devriez pas penser beaucoup aux performances avant d'avoir chronométré votre code et trouvé qu'il s'agit d'un goulot d'étranglement, mais la différence sera là.

L'autre surcharge qui pourrait s'appliquer est que le lambda est forcé d'accéder à une variable de portée ( value). C'est plus lent que d'accéder à une variable locale et dans Python 2.x, la compréhension de la liste accède uniquement aux variables locales. Si vous utilisez Python 3.x, la compréhension de la liste s'exécute dans une fonction distincte, elle sera donc également accessible valuevia une fermeture et cette différence ne s'appliquera pas.

L'autre option à considérer est d'utiliser un générateur au lieu d'une compréhension de liste:

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

Ensuite, dans votre code principal (où la lisibilité importe vraiment), vous avez remplacé la compréhension de la liste et le filtre par un nom de fonction, espérons-le, significatif.


68
+1 pour le générateur. J'ai un lien à la maison vers une présentation qui montre à quel point les générateurs peuvent être incroyables. Vous pouvez également remplacer la compréhension de la liste avec une expression de générateur en changeant []à (). De plus, je suis d'accord que la liste de composition est plus belle.
Wayne Werner

1
En fait, aucun filtre n'est plus rapide. Exécutez simplement quelques benchmarks rapides en utilisant quelque chose comme stackoverflow.com/questions/5998245/…
skqr

2
@skqr préférable d'utiliser simplement timeit pour les tests de performances, mais veuillez donner un exemple où vous trouvez filterêtre plus rapide en utilisant une fonction de rappel Python.
Duncan

8
@ tnq177 C'est la présentation de David Beasley sur les générateurs - dabeaz.com/generators
Wayne Werner

2
@ VictorSchröder oui, peut-être que je n'étais pas clair. Ce que j'essayais de dire, c'est que dans le code principal, vous devez pouvoir voir la situation dans son ensemble. Dans la petite fonction d'aide, il vous suffit de vous soucier de cette fonction, ce qui se passe à l'extérieur peut être ignoré.
Duncan

238

Il s'agit d'un problème quelque peu religieux en Python. Même si Guido a envisagé de supprimer map, filteret reducede Python 3 , il y avait suffisamment de jeu qui, à la fin, n'a reduceété déplacé des fonctions intégrées que vers functools.reduce .

Personnellement, je trouve les listes de compréhension plus faciles à lire. Il est plus explicite ce qui se passe à partir de l'expression [i for i in list if i.attribute == value]car tout le comportement est à la surface et non à l'intérieur de la fonction de filtre.

Je ne m'inquiéterais pas trop de la différence de performance entre les deux approches car elle est marginale. Je n'optimiserais vraiment cela que s'il s'avérait être le goulot d'étranglement dans votre application, ce qui est peu probable.

De plus, puisque le BDFL voulait sortirfilter du langage, cela rendait sûrement automatiquement les compréhensions de liste plus Pythonic ;-)


1
Merci pour les liens vers l'entrée de Guido, si rien d'autre pour moi, cela signifie que j'essaierai de ne plus les utiliser, afin de ne pas prendre l'habitude et de ne pas soutenir cette religion :)
Dashesy

1
mais réduire est le plus complexe à faire avec des outils simples! la carte et le filtre sont triviaux à remplacer par des compréhensions!
njzk2

8
ne savait pas que la réduction avait été rétrogradée en Python3. merci pour la perspicacité! réduire () est toujours très utile en informatique distribuée, comme PySpark. Je pense que c'était une erreur ..
Tagar

1
@Tagar vous pouvez toujours utiliser la réduction, il vous suffit de l'importer à partir de functools
icc97

69

Étant donné que toute différence de vitesse est nécessairement minuscule, l'utilisation de filtres ou de listes de listes se résume à une question de goût. En général, je suis enclin à utiliser des compréhensions (ce qui semble être d'accord avec la plupart des autres réponses ici), mais il y a un cas où je préfèrefilter .

Un cas d'utilisation très fréquent consiste à extraire les valeurs de certains X itérables soumis à un prédicat P (x):

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

mais parfois vous voulez d'abord appliquer une fonction aux valeurs:

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


Comme exemple spécifique, considérez

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

Je pense que cela semble légèrement meilleur que l'utilisation filter. Mais considérez maintenant

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

Dans ce cas, nous voulons filtercomparer la valeur post-calculée. Outre le problème du calcul du cube deux fois (imaginez un calcul plus coûteux), il y a le problème de l'écriture de l'expression deux fois, violant la esthétique SEC . Dans ce cas, je serais apte à utiliser

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

7
N'envisageriez-vous pas d'utiliser le premier via une autre compréhension de la liste? Tels que[prime(i) for i in [x**3 for x in range(1000)]]
viki.omega9

21
x*x*xne peut pas être un nombre premier, comme il l'a fait x^2et xcomme facteur, l'exemple n'a pas vraiment de sens sur le plan mathématique, mais peut-être qu'il est toujours utile. (Peut-être pourrions-nous trouver quelque chose de mieux?)
Zelphir Kaltstahl

3
Notez que nous pouvons utiliser une expression de générateur à la place pour le dernier exemple si nous ne voulons pas manger de mémoire:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
Mateen Ulhaq

4
@MateenUlhaq, cela peut être optimisé pour prime_cubes = [1]économiser à la fois les cycles de mémoire et de processeur ;-)
Dennis Krupenik

7
@DennisKrupenik Ou plutôt,[]
Mateen Ulhaq

29

Bien que cela filterpuisse être la "voie la plus rapide", la "voie Pythonic" ne serait pas de se soucier de telles choses à moins que les performances ne soient absolument critiques (auquel cas vous n'utiliseriez pas Python!).


10
Commentaire tardif à un argument souvent vu: Parfois, cela fait une différence d'avoir une analyse exécutée en 5 heures au lieu de 10, et si cela peut être réalisé en prenant une heure d'optimisation de code python, cela peut valoir la peine (surtout si l'on est confortable avec python et non avec des langages plus rapides).
bli

Mais le plus important est à quel point le code source nous ralentit en essayant de le lire et de le comprendre!
thoni56

20

Je pensais simplement ajouter qu'en python 3, filter () est en fait un objet itérateur, vous devez donc passer votre appel de méthode de filtrage à list () afin de construire la liste filtrée. Donc en 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)

les listes b et c ont les mêmes valeurs et ont été complétées à peu près en même temps que filter () était équivalent [x pour x dans y si z]. Cependant, en 3, ce même code laisserait la liste c contenant un objet filtre, pas une liste filtrée. Pour produire les mêmes valeurs en 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))

Le problème est que list () prend un itérable comme argument et crée une nouvelle liste à partir de cet argument. Le résultat est que l'utilisation de filtre de cette manière en python 3 prend jusqu'à deux fois plus de temps que la méthode [x pour x en y si z] car vous devez répéter la sortie de filter () ainsi que la liste d'origine.


13

Une différence importante est que la compréhension de la liste renverra un listtandis que le filtre renvoie un filter, que vous ne pouvez pas manipuler comme un list(c'est-à-dire: appeler lendessus, qui ne fonctionne pas avec le retour defilter ).

Mon auto-apprentissage m'a amené à un problème similaire.

Cela étant dit, s'il existe un moyen d'avoir le résultat listd'un filter, un peu comme vous le feriez dans .NET lorsque vous le faiteslst.Where(i => i.something()).ToList() , je suis curieux de le savoir.

EDIT: C'est le cas pour Python 3, pas 2 (voir la discussion dans les commentaires).


4
filter renvoie une liste et nous pouvons utiliser len dessus. Au moins dans mon Python 2.7.6.
thiruvenkadam

7
Ce n'est pas le cas en 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
"s'il existe un moyen d'avoir la liste résultante ... je suis curieux de la connaître". Il suffit d' appeler list()sur le résultat: list(filter(my_func, my_iterable)). Et bien sûr, vous pouvez remplacer listpar set, ou tuple, ou toute autre chose qui prend un itérable. Mais pour quiconque autre que les programmeurs fonctionnels, le cas est encore plus fort d'utiliser une compréhension de liste plutôt qu'une filterconversion explicite vers list.
Steve Jessop

10

Je trouve la deuxième façon plus lisible. Il vous indique exactement quelle est l'intention: filtrer la liste.
PS: n'utilisez pas 'list' comme nom de variable


7

filterest généralement légèrement plus rapide si vous utilisez une fonction intégrée.

Je m'attendrais à ce que la compréhension de la liste soit légèrement plus rapide dans votre cas


python -m timeit 'filter (lambda x: x in [1,2,3,4,5], range (10000000))' 10 boucles, meilleur de 3: 1,44 sec par boucle python -m timeit '[x for x dans la plage (10000000) si x dans [1,2,3,4,5]] '10 boucles, meilleur de 3: 860 ms par boucle Pas vraiment?!
giaosudau

@sepdau, les fonctions lambda ne sont pas intégrées. La compréhension des listes s'est améliorée au cours des 4 dernières années - maintenant la différence est de toute façon négligeable même avec les fonctions intégrées
John La Rooy

7

Le filtre n'est que cela. Il filtre les éléments d'une liste. Vous pouvez voir que la définition mentionne la même chose (dans le lien de documentation officiel que j'ai mentionné précédemment). Alors que la compréhension de liste est quelque chose qui produit une nouvelle liste après avoir agi sur quelque chose de la liste précédente. , par exemple, un type de données entièrement nouveau. Comme la conversion d'entiers en chaîne, etc.)

Dans votre exemple, il est préférable d'utiliser le filtre que la compréhension de liste, selon la définition. Cependant, si vous le souhaitez, par exemple other_attribute à partir des éléments de la liste, dans votre exemple doit être récupéré en tant que nouvelle liste, vous pouvez utiliser la compréhension de la liste.

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

C'est ainsi que je me souviens de la compréhension des filtres et des listes. Supprimez quelques éléments d'une liste et conservez les autres éléments intacts, utilisez le filtre. Utilisez vous-même un peu de logique au niveau des éléments et créez une liste édulcorée adaptée à un objectif, utilisez la compréhension de la liste.


2
Je serai heureux de connaître la raison du vote négatif afin de ne plus le répéter nulle part à l'avenir.
thiruvenkadam

la définition du filtre et la compréhension de la liste n'étaient pas nécessaires, car leur signification n'était pas débattue. Le fait qu'une compréhension de liste ne devrait être utilisée que pour les «nouvelles» listes est présenté mais non argumenté.
Agos

J'ai utilisé la définition pour dire que le filtre vous donne une liste avec les mêmes éléments qui sont vrais pour un cas, mais avec la compréhension de la liste, nous pouvons modifier les éléments eux-mêmes, comme la conversion de int en str. Mais point pris :-)
thiruvenkadam

4

Voici une courte pièce que j'utilise lorsque j'ai besoin de filtrer quelque chose après la compréhension de la liste. Juste une combinaison de filtre, lambda et listes (autrement connu comme la loyauté d'un chat et la propreté d'un chien).

Dans ce cas, je lis un fichier, je supprime les lignes vides, les lignes commentées et tout ce qui suit un commentaire sur une ligne:

# 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])]

Cela fait beaucoup de choses en très peu de code. Je pense que cela pourrait être un peu trop logique en une seule ligne pour être facilement compris et la lisibilité est ce qui compte.
Zelphir Kaltstahl

Vous pouvez écrire ceci commefile_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines)))
Steve Jessop

4

En plus de la réponse acceptée, il y a un cas d'angle où vous devez utiliser un filtre au lieu d'une compréhension de liste. Si la liste n'est pas partageable, vous ne pouvez pas la traiter directement avec une compréhension de liste. Un exemple réel est si vous utilisez pyodbcpour lire les résultats d'une base de données. Le fetchAll()résultat cursorest une liste non partageable. Dans cette situation, pour manipuler directement les résultats retournés, un filtre doit être utilisé:

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) 

Si vous utilisez la compréhension de liste ici, vous obtiendrez l'erreur:

TypeError: type non partageable: 'liste'


2
toutes les listes ne sont pas partageables >>> hash(list()) # TypeError: unhashable type: 'list'deuxièmement, cela fonctionne très bien:processed_data = [s for s in data_from_db if 'abc' in s.field1 or s.StartTime >= start_date_time]
Thomas Grainger

1
"Si la liste n'est pas partageable, vous ne pouvez pas la traiter directement avec une compréhension de la liste." Ce n'est pas vrai, et toutes les listes sont de toute façon inébranlables.
juanpa.arrivillaga

3

Il m'a fallu un certain temps pour me familiariser avec le higher order functions filteret map. Alors je filterme suis habitué à eux et j'ai vraiment aimé car il était explicite qu'il filtre en gardant ce qui est vrai et je me sentais cool que j'en connaissaisfunctional programming termes.

J'ai ensuite lu ce passage (Fluent Python Book):

Les fonctions de carte et de filtre sont toujours intégrées dans Python 3, mais depuis l'introduction des listes de compréhension et des expressions de générateur, elles ne sont pas aussi importantes. Un listcomp ou un genexp fait le travail de la carte et du filtre combinés, mais est plus lisible.

Et maintenant, je pense, pourquoi s'embêter avec le concept de filter/ mapsi vous pouvez y parvenir avec des idiomes déjà largement répandus comme les listes de compréhension. De plus maps, ce filterssont des fonctions. Dans ce cas, je préfère utiliser des Anonymous functionslambdas.

Enfin, juste pour le faire tester, j'ai chronométré les deux méthodes ( mapet listComp) et je n'ai vu aucune différence de vitesse pertinente qui justifierait de faire des arguments à ce sujet.

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

0

Curieusement sur Python 3, je vois que le filtre fonctionne plus rapidement que les compréhensions de liste.

J'ai toujours pensé que les listes de compréhension seraient plus performantes. Quelque chose comme: [nom pour le nom dans brand_names_db si le nom n'est pas None] Le bytecode généré est un peu meilleur.

>>> 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

Mais ils sont en réalité plus lents:

   >>> 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

8
Comparaison non valide . Tout d'abord, vous ne passez pas une fonction lambda à la version de filtre, ce qui la rend par défaut à la fonction d'identité. Lors de la définition if not Nonedans la compréhension de la liste que vous êtes en train de définir une fonction lambda (remarquez la MAKE_FUNCTIONdéclaration). Deuxièmement, les résultats sont différents, car la version de compréhension de liste supprimera uniquement la Nonevaleur, tandis que la version de filtre supprimera toutes les valeurs "fausses". Cela dit, tout le but de la micro-analyse comparative est inutile. Ce sont un million d'itérations, multiplié par 1 000 articles! La différence est négligeable .
Victor Schröder

-7

Ma prise

def filter_list(list, key, value, limit=None):
    return [i for i in list if i[key] == value][:limit]

3
in'a jamais été considéré comme un dict, et il n'y en a pas besoin limit. En dehors de cela, en quoi est-ce différent de ce que le PO a suggéré et comment cela répond-il à la question?
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.