web-dev-qa-db-fra.com

Liste compréhension vs carte

Existe-t-il une raison de préférer utiliser map() à la compréhension de liste ou inversement? Est-ce que l'un ou l'autre est généralement plus efficace ou considéré généralement plus pythonique que l'autre?

653
TimothyAWiseman

map peut être microscopiquement plus rapide dans certains cas (lorsque vous ne créez PAS un lambda à cette fin, mais que vous utilisez la même fonction dans map et dans un listcomp). La compréhension des listes peut être plus rapide dans d’autres cas et la plupart des pythonistes (pas tous) les considèrent plus directes et plus claires.

Un exemple de l’avantage minime de la carte en termes de vitesse lorsque vous utilisez exactement la même fonction:

$ python -mtimeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -mtimeit -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 -mtimeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -mtimeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop
598
Alex Martelli

Cas

  • Cas fréquent: Vous voudrez presque toujours utiliser une liste de compréhension en python car ce que vous 'verrez de plus en plus Faites-le aux programmeurs novices qui lisent votre code. (Cela ne s'applique pas aux autres langages, où d'autres idiomes peuvent s'appliquer.) Ce que vous ferez aux programmeurs python sera encore plus évident, car la compréhension de liste est la norme de facto en python pour l'itération; ils sont attendus .
  • Cas moins commun: Cependant, si vous avez déjà une fonction définie , il est souvent raisonnable d'utiliser map , bien qu'il soit considéré comme "non rythmique". Par exemple, map(sum, myLists) est plus élégant que [sum(x) for x in myLists]. Vous avez l’élégance de ne pas avoir à créer une variable factice (par exemple, sum(x) for x... ou sum(_) for _... ou sum(readableName) for readableName...) que vous devez taper deux fois, pour pouvoir le parcourir. Le même argument est valable pour filter et reduce, ainsi que pour le module itertools: si vous avez déjà une fonction à portée de main, vous pouvez procéder à une programmation fonctionnelle. Cela gagne en lisibilité dans certaines situations et le perd dans d'autres (par exemple, les programmeurs débutants, plusieurs arguments) ... mais la lisibilité de votre code dépend fortement de vos commentaires.
  • Presque jamais: Vous voudrez peut-être utiliser la fonction map comme une fonction abstraite pure lors de la programmation fonctionnelle, où vous mappez map ou courrez map , ou tire avantage de parler de map en tant que fonction. En Haskell, par exemple, une interface de foncteur appelée fmap généralise le mappage sur toute structure de données. Ceci est très rare dans python parce que la grammaire python vous oblige à utiliser le style de générateur pour parler d'itération; vous ne pouvez pas généraliser facilement. (C'est parfois bon et parfois mauvais.) Vous pouvez probablement trouver de rares exemples de python où map(f, *lists) est une chose raisonnable à faire. L'exemple le plus proche que je puisse citer serait sumEach = partial(map,sum), qui est une ligne qui équivaut à peu près à:
def sumEach(myLists):
    return [sum(_) for _ in myLists]
  • En utilisant simplement une boucle for-: Vous pouvez aussi bien sûr utiliser une boucle for. Bien qu'elles ne soient pas aussi élégantes du point de vue de la programmation fonctionnelle, des variables parfois non locales rendent le code plus clair dans les langages de programmation impératifs tels que python, car les utilisateurs sont très habitués à lire le code de cette façon. Les boucles for sont également, en règle générale, les plus efficaces lorsque vous effectuez simplement une opération complexe qui ne construit pas une liste, car la liste comprend et la carte sont optimisées (sommation, création d’un arbre, etc.) - du moins efficace en termes de mémoire (pas nécessairement en termes de temps, où je m'attendrais au pire à un facteur constant, à l'exception de quelques ratés pathologiques rares liés au ramassage des ordures).

"Pythonisme"

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

paresse

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

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

Essayez de faire cela avec une compréhension de liste:

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

Notez que les interprétations de liste sont aussi fondamentalement paresseuses, mais python a choisi de les implémenter comme non paresseuses . Néanmoins, python prend en charge les interprétations de liste paresseuse sous la forme d'expressions génératrices, comme suit:

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

En gros, vous pouvez considérer la syntaxe [...] comme transmettant 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 ne sont pas paresseuses et peuvent donc nécessiter plus de mémoire (sauf si vous utilisez des compréhensions de générateur). Les crochets [...] rendent souvent les choses évidentes, en particulier dans un fouillis de parenthèses. D'autre part, vous finissez parfois par être bavard, comme si vous tapiez [x for x in.... Tant que vos variables d'itérateur sont courtes, la compréhension de la liste est généralement plus claire 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 casser des choses:

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

Comparaison de l'efficacité pour python

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'utilisez pas toutes vos données ou ne savez pas à l'avance la quantité de données dont vous avez besoin, map dans python3 (et les expressions de générateur dans python2 ou python3) évitez de calculer leurs valeurs jusqu'au dernier moment nécessaire. Habituellement, l’utilisation de map l'emporte sur les frais généraux. L'inconvénient est que cela est très limité dans python par opposition à la plupart des langages fonctionnels: vous n'obtenez cet avantage que si vous accédez à vos données de gauche à droite "dans l'ordre", car python Les expressions du générateur ne peuvent être évaluées que dans l'ordre x[0], x[1], x[2], ....

Cependant, supposons que nous ayons une fonction prédéfinie f que 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 sous la forme AAA/BBB/CCC où A a été exécuté avec un poste de travail Intel circa-2010 avec python 3.?.?, Et B et C ont été exécutés avec un poste de travail AMD circa-2013. avec python 3.2.1, avec un matériel extrêmement différent. Le résultat semble être que les interprétations des cartes et des listes sont comparables en termes de performances, qui sont le plus fortement affectées par d'autres facteurs aléatoires. La seule chose que nous pouvons dire, c’est que curieusement, alors que nous nous attendons à ce que la compréhension de liste [...] soit plus performante que les expressions génératrices (...), map est AUSSI plus efficace que les expressions génératrices (à nouveau que toutes les valeurs sont évaluées/utilisées).

Il est important de réaliser que ces tests assument une fonction très simple (la fonction identité); toutefois, cela convient car, si la fonction était compliquée, les frais généraux de performance seraient négligeables par rapport aux autres facteurs du programme. (Il peut être intéressant de tester avec d'autres choses simples comme f=lambda x:x+x)

Si vous savez lire python Assembly, vous pouvez utiliser le module dis pour voir si c'est ce qui se passe dans les 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 vaut mieux utiliser la syntaxe [...] que list(...). Malheureusement, la classe map est un peu opaque au désassemblage, mais notre test de vitesse peut nous rendre à l’écart.

429
ninjagecko

Python 2: Vous devriez utiliser map et filter à la place de la compréhension par liste.

Une objectif raison pour laquelle vous devriez les préférer même s’ils ne sont pas "Pythonic" est la suivante:
Ils ont besoin des fonctions/lambdas comme arguments, qui introduisent une nouvelle portée.

J'ai été mordu par cela 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 au lieu de cela j'avais dit:

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

alors tout aurait été bien.

Vous pourriez dire que j'étais idiot d'utiliser le même nom de variable dans la même portée.

Je n'étais pas. Le code était correct à l'origine - les deux xs n'étaient pas dans la même portée.
C’est seulement après que I déplacé le bloc interne vers une autre section du code que le problème est survenu (lire: problème pendant la maintenance, pas le développement), et je ne m'attendais pas il.

Oui, si vous ne faites jamais cette erreur , alors les compréhensions de liste sont plus élégantes.
Mais d’expérience personnelle (et de voir les autres commettre la même erreur), j’ai vu cela se produire assez de fois pour que je pense que cela ne vaut pas la peine que vous subissez lorsque ces bugs se glissent dans votre code.

Conclusion:

Utilisez map et filter. Ils empêchent les bogues liés à la portée difficiles à diagnostiquer.

Note de côté:

N'oubliez pas d'envisager d'utiliser imap et ifilter (dans itertools) si elles conviennent à votre situation!

89
Mehrdad

En fait, map et les interprétations de liste se comportent assez différemment 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))

Vous pouvez vous attendre à ce qu'il imprime la ligne "[1, 4, 9]" deux fois, mais à la place, il affiche "[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 Python 2 langage map renvoie une liste ancienne et claire, comme le font les interprétations de liste dans les deux langues. Le point crucial est que la valeur de retour de map dans Python 3 (et imap dans Python 2) n'est pas une liste - c'est un itérateur!

Les éléments sont consommés lorsque vous parcourez un itérateur contrairement à lorsque vous parcourez une liste. C'est pourquoi squares semble vide dans la dernière ligne print(list(squares)).

Résumer:

  • Lorsque vous utilisez des itérateurs, vous devez vous rappeler qu'ils sont à état et qu'ils mutent au fur et à mesure que vous les traversez.
  • Les listes sont plus prévisibles car elles ne changent que lorsque vous les modifiez explicitement. ce sont des conteneurs .
  • Et un bonus: les nombres, les chaînes et les n-uplets sont encore plus prévisibles car ils ne peuvent pas changer du tout; ce sont des valeurs .
40
raek

Je trouve que les compréhensions de liste expriment généralement mieux ce que j'essaie de faire que map - elles le font toutes les deux, mais le premier évite la charge mentale d'essayer de comprendre ce que pourrait être un complexe lambda expression.

Il y a aussi une interview quelque part (je ne trouve pas ça tout à fait) où Guido cite lambdas et les fonctions fonctionnelles comme ce qu'il regrette le plus d'avoir accepté en Python, afin que vous puissiez argumenter qu'elles ne le sont pas. -Pythonique en vertu de cela.

16
Dan

Si vous envisagez d'écrire du code asynchrone, parallèle ou distribué, vous préférerez probablement map à une compréhension de liste. En effet, la plupart des packages asynchrones, parallèles ou distribués fournissent une fonction map pour surcharger les propriétés de python map. Ensuite, en transmettant la fonction map appropriée au reste de votre code, vous n'avez peut-être pas à modifier votre code série d'origine pour le faire fonctionner en parallèle (etc.).

15
Mike McKerns

Voici un cas possible:

map(lambda op1,op2: op1*op2, list1, list2)

contre:

[op1*op2 for op1,op2 in Zip(list1,list2)]

Je suppose que le Zip () est une surcharge inutile et inutile que vous devez vous laisser aller si vous insistez pour utiliser des listes de compréhension au lieu de la carte. Ce serait bien si quelqu'un clarifiait cela, que ce soit affirmativement ou négativement.

15
Andz

Donc, puisque Python 3, map() est un itérateur, vous devez garder à l'esprit ce dont vous avez besoin: un itérateur ou un objet list.

Comme @AlexMartelli déjà mentionné , map() est plus rapide que la compréhension de la liste uniquement si vous n'utilisez pas la fonction lambda.

Je vais vous présenter quelques comparaisons de temps.

Python 3.5.2 et CPython
J'ai utilisé bloc-notes Jupiter et surtout %timeit commande magique intégrée
Mesures : s == 1000 ms == 1000 * 1000 µs = 1000 * 1000 * 1000 ns

Installer:

_x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))
_

Fonction intégrée:

_%timeit map(sum, x_list)  # creating iterator object
# Output: The slowest run took 9.91 times longer than the fastest. 
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 277 ns per loop

%timeit list(map(sum, x_list))  # creating list with map
# Output: 1000 loops, best of 3: 214 µs per loop

%timeit [sum(x) for x in x_list]  # creating list with list comprehension
# Output: 1000 loops, best of 3: 290 µs per loop
_

lambda fonction:

_%timeit map(lambda i: i+1, i_list)
# Output: The slowest run took 8.64 times longer than the fastest. 
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 325 ns per loop

%timeit list(map(lambda i: i+1, i_list))
# Output: 1000 loops, best of 3: 183 µs per loop

%timeit [i+1 for i in i_list]
# Output: 10000 loops, best of 3: 84.2 µs per loop
_

Il existe également une expression générique, voir PEP-0289 . J'ai donc pensé qu'il serait utile de l'ajouter à la comparaison

_%timeit (sum(i) for i in x_list)
# Output: The slowest run took 6.66 times longer than the fastest. 
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 495 ns per loop

%timeit list((sum(x) for x in x_list))
# Output: 1000 loops, best of 3: 319 µs per loop

%timeit (i+1 for i in i_list)
# Output: The slowest run took 6.83 times longer than the fastest. 
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 506 ns per loop

%timeit list((i+1 for i in i_list))
# Output: 10000 loops, best of 3: 125 µs per loop
_

Vous avez besoin de l'objet list:

Utilisez la compréhension de liste si c'est une fonction personnalisée, utilisez list(map()) s'il y a une fonction intégrée

Vous n'avez pas besoin de l'objet list, vous avez simplement besoin d'un objet itératif:

Toujours utiliser map()!

6
vishes_shell

J'ai effectué un test rapide comparant trois méthodes pour invoquer la méthode d'un objet. La différence de temps, dans ce cas, est négligeable et relève de la fonction en question (voir @ Alex Martelli réponse ). Ici, j'ai regardé les méthodes suivantes:

# map_lambda
list(map(lambda x: x.add(), vals))

# map_operator
from operator import methodcaller
list(map(methodcaller("add"), vals))

# map_comprehension
[x.add() for x in vals]

J'ai consulté des listes (stockées dans la variable vals) de nombres entiers (Python int) et de nombres à virgule flottante (Python float) pour augmenter la taille des listes. La classe factice suivante DummyNum est considérée:

class DummyNum(object):
    """Dummy class"""
    __slots__ = 'n',

    def __init__(self, n):
        self.n = n

    def add(self):
        self.n += 5

Plus précisément, la méthode add. L'attribut __slots__ est une optimisation simple dans Python pour définir la mémoire totale nécessaire à la classe (attributs), réduisant ainsi sa taille. Voici les parcelles résultantes.

Performance of mapping Python object methods

Comme indiqué précédemment, la technique utilisée fait une différence minimale et vous devez coder de la manière la plus lisible pour vous, ou dans les circonstances particulières. Dans ce cas, la compréhension de la liste (technique map_comprehension) est la plus rapide pour les deux types d’ajouts dans un objet, en particulier avec des listes plus courtes.

Visitez this Pastebin pour la source utilisée pour générer le tracé et les données.

0
craymichael

Je considère que le moyen le plus pythonique consiste à utiliser une liste de compréhension au lieu de map et filter. La raison en est que la compréhension des listes est plus claire que map et filter.

In [1]: odd_cubes = [x ** 3 for x in range(10) if x % 2 == 1] # using a list comprehension

In [2]: odd_cubes_alt = list(map(lambda x: x ** 3, filter(lambda x: x % 2 == 1, range(10)))) # using map and filter

In [3]: odd_cubes == odd_cubes_alt
Out[3]: True

Comme vous le voyez, une compréhension ne nécessite pas d'expressions lambda supplémentaires comme map. De plus, une compréhension permet également de filtrer facilement, alors que map nécessite filter pour permettre le filtrage.

0
lmiguelvargasf