834 votes

Compréhension de liste vs carte

Y a-t-il une raison de préférer utiliser map() sur la compréhension des listes ou vice versa ? L'un ou l'autre est-il généralement plus efficace ou considéré comme plus pythique que l'autre ?

8 votes

Notez que PyLint vous avertit si vous utilisez la compréhension par carte au lieu de la compréhension par liste, voir message W0141 .

3 votes

@lumbric, je ne suis pas sûr mais cela ne fonctionne que si lambda est utilisé dans map.

2 votes

J'ai réalisé un tutoriel de 17 minutes sur la comparaison entre liste et carte si quelqu'un le trouve utile. youtube.com/watch?v=hNW6Tbp59HQ

746voto

Alex Martelli Points 330805

map peut être microscopiquement plus rapide dans certains cas (lorsque vous ne créez PAS un lambda dans ce but, mais que vous utilisez la même fonction dans map et dans une comprehension de liste). Les compréhensions de listes peuvent être plus rapides dans d'autres cas et la plupart (pas tous) des pythonistes les considèrent plus directes et plus claires.

Un exemple de l'avantage minuscule de rapidité de map lorsqu'on utilise exactement la même fonction :

$ python -m timeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -m timeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop

Un exemple de la façon dont la comparaison des performances est complètement inversée lorsque map a besoin d'un lambda :

$ python -m timeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -m timeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop

1 votes

Merci. Il semble que les compréhensions de listes soient la meilleure solution dans la plupart des cas.

52 votes

Oui, en effet, notre guide de style Python interne au travail recommande explicitement les listcomps contre map et filter (sans même mentionner l'amélioration minuscule mais mesurable des performances que map peut donner dans certains cas;-).

51 votes

Je ne veux pas casser les points de style infinis d'Alex, mais parfois map me semble plus facile à lire : data = map(str, some_list_of_objects). Quelques autres... operator.attrgetter, operator.itemgetter, etc.

521voto

ninjagecko Points 25709

Cas

  • Cas commun : Dans la plupart des cas, vous voudrez utiliser une liste de compréhension en python car ce que vous faites sera plus évident pour les programmeurs novices qui lisent votre code. (Ceci ne s'applique pas aux autres langages, où d'autres idiomes peuvent s'appliquer). Ce que vous faites sera même plus évident pour les programmeurs python, puisque les compréhensions de listes sont la norme de facto en python pour l'itération ; elles sont les suivantes attendu .

  • Cas moins fréquent : Cependant, si vous ont déjà une fonction définie il est souvent raisonnable d'utiliser map bien qu'il soit considéré comme "non pythonique". Par exemple, map(sum, myLists) est plus élégant/terse que [sum(x) for x in myLists] . Vous gagnez l'élégance de ne pas avoir à inventer une variable fictive (par ex. sum(x) for x... o sum(_) for _... o sum(readableName) for readableName... ) que vous devez taper deux fois, juste pour itérer. Le même argument vaut pour filter y reduce et tout ce qui concerne le itertools module : si vous avez déjà une fonction à portée de main, vous pouvez aller de l'avant et faire de la programmation fonctionnelle. Cela permet de gagner en lisibilité dans certaines situations, et d'en perdre dans d'autres (par exemple, programmeurs novices, arguments multiples)... mais la lisibilité de votre code dépend fortement de vos commentaires de toute façon.

  • Presque jamais. : Vous pouvez utiliser le map comme une fonction abstraite pure tout en faisant de la programmation fonctionnelle, où vous mettez en correspondance map ou le curry map ou bénéficier d'une discussion sur map comme une fonction. En Haskell par exemple, une interface de type foncteur appelée fmap généralise le mappage sur n'importe quelle structure de données. C'est très rare en python parce que la grammaire python vous oblige à utiliser le style générateur pour parler de l'itération ; vous ne pouvez pas le généraliser facilement. (C'est parfois une bonne et parfois une mauvaise chose.) Vous pouvez probablement trouver de rares exemples en python où map(f, *lists) est une chose raisonnable à faire. L'exemple le plus proche que je puisse trouver est le suivant sumEach = partial(map,sum) ce qui équivaut à peu près à :

    def sumEach(myLists): return [sum() for in myLists]

  • Il suffit d'utiliser un for -boucle : Vous pouvez aussi bien sûr simplement utiliser une boucle for. Bien qu'elles ne soient pas aussi élégantes du point de vue de la programmation fonctionnelle, les variables non-locales rendent parfois le code plus clair dans les langages de programmation impératifs tels que python, car les gens sont habitués à lire le code de cette manière. Les for-loops sont aussi, généralement, les plus efficaces lorsque vous faites simplement une opération complexe qui n'est pas la construction d'une liste comme les comprehensions de liste et map sont optimisées pour (par exemple additionner, ou faire un arbre, etc.) -- au moins efficace en termes de mémoire (pas nécessairement en termes de temps, où je m'attendrais au pire à un facteur constant, à moins d'un rare hoquet pathologique de garbage-collection).

"Pythonisme"

Je n'aime pas le mot "python" car je ne trouve pas que le python soit toujours élégant à mes yeux. Néanmoins, map y filter et des fonctions similaires (comme la très utile itertools ) sont probablement considérés comme non pythoniques en termes de style.

Paresse

En termes d'efficacité, comme la plupart des constructions de programmation fonctionnelle, LA CARTE PEUT ÊTRE PARESSEUSE et est en fait paresseux en python. Cela signifie que vous pouvez faire ceci (dans python3 ) et votre ordinateur ne manquera pas de mémoire et ne perdra pas toutes vos données non sauvegardées :

>>> map(str, range(10**100))
<map object at 0x2201d50>

Essayez de faire ça avec la compréhension d'une liste :

>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #

Notez que les comprehensions de listes sont aussi intrinsèquement paresseuses, mais python a choisi de les implémenter comme non-lazy. . Néanmoins, python prend en charge les compréhensions de listes paresseuses sous la forme d'expressions de générateur, comme suit :

>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>

En gros, vous pouvez penser à la [...] comme le passage d'une expression de générateur au constructeur de liste, comme list(x for x in range(5)) .

Bref exemple artificiel

from operator import neg
print({x:x**2 for x in map(neg,range(5))})

print({x:x**2 for x in [-y for y in range(5)]})

print({x:x**2 for x in (-y for y in range(5))})

Les compréhensions de liste sont non-lazy, et peuvent donc nécessiter plus de mémoire (à moins que vous n'utilisiez des compréhensions de générateur). Les crochets [...] rendent souvent les choses évidentes, surtout lorsqu'elles se trouvent dans un fouillis de parenthèses. D'un autre côté, il arrive que l'on finisse par être verbeux, comme en tapant [x for x in... . Tant que vous gardez vos variables d'itérateurs courtes, les compréhensions de listes sont généralement plus claires si vous n'indentez pas votre code. Mais vous pouvez toujours indenter votre code.

print(
    {x:x**2 for x in (-y for y in range(5))}
)

ou de casser les choses :

rangeNeg5 = (-y for y in range(5))
print(
    {x:x**2 for x in rangeNeg5}
)

Comparaison de l'efficacité de python3

map est maintenant paresseux :

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=map(f,xs)'
1000000 loops, best of 3: 0.336 usec per loop            ^^^^^^^^^

Par conséquent, si vous n'allez pas utiliser toutes vos données, ou si vous ne savez pas à l'avance de combien de données vous avez besoin, map en python3 (et les expressions de générateur en python2 ou python3) éviteront de calculer leurs valeurs jusqu'au dernier moment nécessaire. Habituellement, cela l'emportera sur la surcharge due à l'utilisation de map . L'inconvénient est que cela est très limité en python par rapport à la plupart des langages fonctionnels : vous ne bénéficiez de cet avantage que si vous accédez à vos données de gauche à droite "dans l'ordre", car les expressions du générateur python ne peuvent être évaluées que dans l'ordre suivant x[0], x[1], x[2], ... .

Toutefois, disons que nous avons une fonction préétablie f nous aimerions map et nous ignorons la paresse de map en forçant immédiatement l'évaluation avec list(...) . Nous obtenons des résultats très intéressants :

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(map(f,xs))'                                                                                                                                                
10000 loops, best of 3: 165/124/135 usec per loop        ^^^^^^^^^^^^^^^
                    for list(<map object>)

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=[f(x) for x in xs]'                                                                                                                                      
10000 loops, best of 3: 181/118/123 usec per loop        ^^^^^^^^^^^^^^^^^^
                    for list(<generator>), probably optimized

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(f(x) for x in xs)'                                                                                                                                    
1000 loops, best of 3: 215/150/150 usec per loop         ^^^^^^^^^^^^^^^^^^^^^^
                    for list(<generator>)

Les résultats sont de la forme AAA/BBB/CCC, où A a été réalisé sur une station de travail Intel datant de 2010 avec python 3. ?.?, et B et C ont été réalisés sur une station de travail AMD datant de 2013 avec python 3.2.1, avec un matériel extrêmement différent. Le résultat semble être que les compréhensions de cartes et de listes sont comparables en termes de performances, qui sont plus fortement affectées par d'autres facteurs aléatoires. La seule chose que nous pouvons dire semble être que, bizarrement, alors que nous nous attendons à ce que les compréhensions de listes [...] pour obtenir de meilleurs résultats que les expressions génératrices (...) , map est AUSSI plus efficace que les expressions de générateur (encore une fois en supposant que toutes les valeurs sont évaluées/utilisées).

Il est important de réaliser que ces tests supposent une fonction très simple (la fonction d'identité) ; cependant, c'est très bien ainsi car si la fonction était compliquée, le surcoût de performance serait négligeable par rapport aux autres facteurs du programme. (Il peut toujours être intéressant de tester avec d'autres choses simples comme f=lambda x:x+x )

Si vous savez lire l'assemblage python, vous pouvez utiliser la fonction dis module pour voir si c'est vraiment ce qui se passe en coulisses :

>>> listComp = compile('[f(x) for x in xs]', 'listComp', 'eval')
>>> dis.dis(listComp)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x2511a48, file "listComp", line 1>) 
              3 MAKE_FUNCTION            0 
              6 LOAD_NAME                0 (xs) 
              9 GET_ITER             
             10 CALL_FUNCTION            1 
             13 RETURN_VALUE         
>>> listComp.co_consts
(<code object <listcomp> at 0x2511a48, file "listComp", line 1>,)
>>> dis.dis(listComp.co_consts[0])
  1           0 BUILD_LIST               0 
              3 LOAD_FAST                0 (.0) 
        >>    6 FOR_ITER                18 (to 27) 
              9 STORE_FAST               1 (x) 
             12 LOAD_GLOBAL              0 (f) 
             15 LOAD_FAST                1 (x) 
             18 CALL_FUNCTION            1 
             21 LIST_APPEND              2 
             24 JUMP_ABSOLUTE            6 
        >>   27 RETURN_VALUE

>>> listComp2 = compile('list(f(x) for x in xs)', 'listComp2', 'eval')
>>> dis.dis(listComp2)
  1           0 LOAD_NAME                0 (list) 
              3 LOAD_CONST               0 (<code object <genexpr> at 0x255bc68, file "listComp2", line 1>) 
              6 MAKE_FUNCTION            0 
              9 LOAD_NAME                1 (xs) 
             12 GET_ITER             
             13 CALL_FUNCTION            1 
             16 CALL_FUNCTION            1 
             19 RETURN_VALUE         
>>> listComp2.co_consts
(<code object <genexpr> at 0x255bc68, file "listComp2", line 1>,)
>>> dis.dis(listComp2.co_consts[0])
  1           0 LOAD_FAST                0 (.0) 
        >>    3 FOR_ITER                17 (to 23) 
              6 STORE_FAST               1 (x) 
              9 LOAD_GLOBAL              0 (f) 
             12 LOAD_FAST                1 (x) 
             15 CALL_FUNCTION            1 
             18 YIELD_VALUE          
             19 POP_TOP              
             20 JUMP_ABSOLUTE            3 
        >>   23 LOAD_CONST               0 (None) 
             26 RETURN_VALUE

>>> evalledMap = compile('list(map(f,xs))', 'evalledMap', 'eval')
>>> dis.dis(evalledMap)
  1           0 LOAD_NAME                0 (list) 
              3 LOAD_NAME                1 (map) 
              6 LOAD_NAME                2 (f) 
              9 LOAD_NAME                3 (xs) 
             12 CALL_FUNCTION            2 
             15 CALL_FUNCTION            1 
             18 RETURN_VALUE 

Il semble qu'il soit préférable d'utiliser [...] syntaxe que list(...) . Malheureusement, le map est un peu opaque au désassemblage, mais nous pouvons nous contenter de notre test de vitesse.

5 votes

"le très utile module itertools [est] probablement considéré comme non pythonique en termes de style". Hmm. Je n'aime pas non plus le terme "Pythonique", donc dans un certain sens je ne me soucie pas de ce qu'il signifie, mais je ne pense pas qu'il soit juste pour ceux qui l'utilisent, de dire que selon la "Pythonicité" les builtins map y filter ainsi que la bibliothèque standard itertools sont intrinsèquement un mauvais style. À moins que GvR ne dise réellement qu'il s'agissait d'une terrible erreur ou d'une simple question de performance, la seule conclusion naturelle si c'est ce que dit "Pythonicité" est de l'oublier comme étant stupide ;-)

8 votes

@SteveJessop : En fait, Guido pensait laisser tomber map / filter était une excellente idée pour Python 3 et seule une rébellion d'autres Pythonistes les a maintenus dans l'espace de noms intégré (alors que reduce a été déplacé vers functools ). Je ne suis personnellement pas d'accord ( map y filter n'ont pas de problème avec les fonctions prédéfinies, en particulier les fonctions intégrées, mais ne les utilisent jamais si une lambda serait nécessaire), mais GvR a essentiellement dit qu'ils n'étaient pas pythoniques pendant des années.

0 votes

@ShadowRanger : c'est vrai, mais est-ce que GvR a déjà prévu de supprimer itertools ? La partie que je cite de cette réponse est la principale affirmation qui me laisse perplexe. Je ne sais pas si dans son monde idéal, map y filter se déplacerait vers itertools (ou functools ) ou disparaître complètement, mais quel que soit le cas, une fois que l'on dit que itertools n'est pas Pythonique dans sa totalité, alors je ne sais pas vraiment ce que "Pythonique" est supposé signifier mais je ne pense pas que cela puisse être quelque chose de similaire à "ce que GvR recommande aux gens d'utiliser".

106voto

Mehrdad Points 70493

Python 2 : Vous devez utiliser map y filter au lieu des compréhensions de listes.

Un site objectif La raison pour laquelle vous devriez les préférer, même s'ils ne sont pas "pythoniques", est la suivante :
Ils requièrent des fonctions/lambdas comme arguments, qui introduire un nouveau champ d'application .

Je me suis fait piquer par ça plus d'une fois :

for x, y in somePoints:
    # (several lines of code here)
    squared = [x ** 2 for x in numbers]
    # Oops, x was silently overwritten!

mais si à la place j'avais dit :

for x, y in somePoints:
    # (several lines of code here)
    squared = map(lambda x: x ** 2, numbers)

alors tout aurait été parfait.

On pourrait dire que j'ai été stupide d'utiliser le même nom de variable dans la même portée.

Je ne l'étais pas. Le code était bien à l'origine - les deux x n'étaient pas dans le même périmètre.
C'est seulement après que j'ai déplacé le bloc interne à une section différente du code que le problème est apparu (lire : problème pendant la maintenance, pas le développement), et je ne m'y attendais pas.

Sí, si vous ne faites jamais cette erreur alors les compréhensions de listes sont plus élégantes.
Mais par expérience personnelle (et pour avoir vu d'autres personnes faire la même erreur), j'ai vu cela se produire suffisamment de fois pour penser que cela ne vaut pas la peine de souffrir lorsque ces bogues se glissent dans votre code.

Conclusion :

Utilisez map y filter . Ils évitent les bogues subtils et difficiles à diagnostiquer liés à la portée.

Note complémentaire :

N'oubliez pas d'envisager d'utiliser imap y ifilter (en itertools ) si elles sont adaptées à votre situation !

7 votes

Merci de me le signaler. Je n'avais pas explicitement pensé que la compréhension des listes était dans le même champ d'application et pouvait poser problème. Cela dit, je pense que certaines des autres réponses indiquent clairement que la compréhension de liste devrait être l'approche par défaut la plupart du temps, mais que c'est quelque chose à ne pas oublier. C'est aussi un bon rappel général pour garder les fonctions (et donc la portée) petites et avoir des tests unitaires complets et utiliser des déclarations assertes.

0 votes

Ou alors, utilisez toujours le générateur et quand vous ne pouvez pas, vous pouvez toujours utiliser list(x+2 for x in objs) . N'est-ce pas ?

1 votes

À mon avis, cette seule raison est un exemple mesquin pour faire une affirmation aussi audacieuse dès la première ligne. C'est un raisonnement complètement invalide dans python 3, et même dans 2.x ce n'est pas exactement un bug subtil pour quiconque a utilisé python plus de quelques mois. C'est probablement le piège le plus connu, après le défaut mutable

48voto

raek Points 1065

En fait, map et les compréhensions de listes se comportent de manière très différente dans le langage Python 3. Jetez un coup d'œil au programme Python 3 suivant :

def square(x):
    return x*x
squares = map(square, [1, 2, 3])
print(list(squares))
print(list(squares))

On pourrait s'attendre à ce qu'il imprime deux fois la ligne "[1, 4, 9]", mais au lieu de cela, il imprime "[1, 4, 9]" suivi de "[]". La première fois que vous regardez squares il semble se comporter comme une séquence de trois éléments, mais la deuxième fois comme une séquence vide.

Dans le langage Python 2 map retourne une simple liste, comme le font les comprehensions de listes dans les deux langues. L'essentiel est que la valeur de retour de map dans Python 3 (et imap en Python 2) n'est pas une liste - c'est un itérateur !

Les éléments sont consommés lorsque vous itérez sur un itérateur, contrairement à ce qui se passe lorsque vous itérez sur une liste. C'est pourquoi squares semble vide dans le dernier print(list(squares)) ligne.

Pour résumer :

  • Lorsque vous traitez avec des itérateurs, vous devez vous rappeler qu'ils ont un état et qu'ils mutent lorsque vous les parcourez.
  • Les listes sont plus prévisibles puisqu'elles ne changent que lorsque vous les mutez explicitement ; elles sont conteneurs .
  • Et un bonus : les nombres, les chaînes de caractères et les tuples sont encore plus prévisibles car ils ne peuvent pas changer du tout ; ils sont valeurs .

0 votes

C'est probablement le meilleur argument pour les compréhensions de listes. La carte de Python n'est pas la carte fonctionnelle, mais le petit enfant roux et handicapé d'une implémentation fonctionnelle. C'est très triste, car je n'aime vraiment pas les compréhensions.

0 votes

@semiomant Je dirais que la lazy map (comme dans python3) est plus 'fonctionnelle' que la eager map (comme dans python2). Par exemple, map en Haskell est lazy (enfin, tout en Haskell est lazy...). Quoi qu'il en soit, la lazy map est meilleure pour enchaîner les maps - si vous avez une map appliquée à map appliquée à map, vous avez une liste pour chacun des appels intermédiaires de map dans python2, alors que dans python3 vous n'avez qu'une seule liste résultante, ce qui est plus efficace en mémoire.

0 votes

Je suppose que ce que je veux c'est que map pour produire une structure de données, et non un itérateur. Mais peut-être que les itérateurs paresseux sont plus faciles que les structures de données paresseuses. De quoi réfléchir. Merci @MnZrK

16voto

Dan Points 564

Je trouve que les compréhensions de listes sont généralement plus expressives de ce que j'essaye de faire que map - ils y parviennent tous les deux, mais le premier évite la charge mentale que représente l'effort de compréhension de ce qui pourrait être un problème complexe. lambda expression.

Il y a aussi une interview quelque part (je ne la trouve pas) où Guido énumère les points suivants lambda et les fonctions fonctionnelles comme la chose qu'il regrette le plus d'avoir accepté dans Python, on pourrait donc dire qu'ils ne sont pas pythoniques pour cette raison.

10 votes

Oui, je soupire, mais l'intention initiale de Guido de supprimer complètement les lambdas dans Python 3 a fait l'objet d'un barrage de lobbying, et il est revenu sur sa décision malgré mon soutien inconditionnel - eh bien, je suppose que les lambdas sont trop pratiques dans de nombreux cas. SIMPLE Dans tous les cas, le seul problème est lorsqu'il excède les limites de l'objectif de l'UE. SIMPLE ou est assigné à un nom (dans ce dernier cas, il s'agit d'un duplicata stupide de def!-).

3 votes

L'entretien auquel vous pensez est celui-là : amk.ca/python/écriture/gvr-interview où Guido dit "Il m'est arrivé d'accepter trop rapidement des contributions, et de me rendre compte plus tard que c'était une erreur. Un exemple serait certaines des caractéristiques de la programmation fonctionnelle, telles que les fonctions lambda. lambda est un mot-clé qui vous permet de créer une petite fonction anonyme ; les fonctions intégrées telles que map, filter et reduce exécutent une fonction sur un type de séquence, comme une liste."

4 votes

@Alex, je n'ai pas vos années d'expérience, mais j'ai vu beaucoup plus de compréhensions de listes trop compliquées que de lambdas. Bien sûr, abuser des fonctionnalités du langage est toujours une tentation à laquelle il est difficile de résister. Il est intéressant de constater que les compilations de listes semblent (empiriquement) plus sujettes aux abus que les lambdas, bien que je ne sois pas sûr de la raison pour laquelle cela devrait être le cas. Je tiens également à souligner que "entravé" n'est pas toujours une mauvaise chose. Réduire l'étendue des "choses que cette ligne pourrait faire" peut parfois rendre les choses plus faciles pour le lecteur. Par exemple, le const en C++ est un grand triomphe dans ce domaine.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X