Liste compréhension vs carte
-
12-09-2019 - |
Question
Y at-il une raison de préférer utiliser map()
sur la compréhension de la liste ou vice-versa? Est-ce l'un d'eux généralement plus efficaces ou plus généralement considérés comme pythonique que l'autre?
La solution
map
peut être plus rapide dans certains microscopiquement cas (quand vous ne faites pas un lambda dans le but, mais en utilisant la même fonction sur la carte et un listcomp). Liste compréhensions peut être plus rapide dans d'autres cas et la plupart (pas tous) Pythoneux les considèrent plus directe et plus claire.
Un exemple de l'avantage minuscule vitesse de carte lors de l'utilisation de 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 se complètement inversée lorsque la carte 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
Autres conseils
Cas
- cas commun : Presque toujours, vous voulez utiliser une compréhension de liste dans python , car il sera plus évident ce que vous faites pour les programmeurs débutants lire votre code . (Cela ne concerne pas les autres langues, où d'autres idiomes peuvent appliquer.) Il sera encore plus évident ce que vous faites pour les programmeurs Python, puisque la liste sont compréhensions la norme de facto en python pour l'itération; ils sont devraient .
- cas moins courants : Toutefois, si vous ont déjà une fonction définie , il est souvent raisonnable d'utiliser
map
, mais il est considéré comme 'unpythonic'. Par exemple,map(sum, myLists)
est plus élégant / laconique que[sum(x) for x in myLists]
. Vous gagnez l'élégance de ne pas avoir à constituer une variable fictive (par exemple desum(x) for x...
ousum(_) for _...
ousum(readableName) for readableName...
) que vous devez taper deux fois, juste pour itérer. Le même argument est valable pourfilter
etreduce
et quoi que ce soit à partir du moduleitertools
: si vous avez déjà une fonction à portée de main, vous pouvez aller de l'avant et faire de la programmation fonctionnelle. Cette lisibilité gagne dans certaines situations, et perd dans d'autres (par exemple, les programmeurs novices, plusieurs arguments) ... mais la lisibilité de votre code dépend fortement de vos commentaires en tout cas. - Presque jamais : Vous pouvez utiliser la fonction
map
en fonction abstraite pur tout en faisant la programmation fonctionnelle, où vous cartographiemap
, ou taitementmap
, ou tout autre avantage de parlermap
comme une fonction. Dans Haskell par exemple, une interface de foncteur appeléfmap
généralise la cartographie sur toute structure de données. Ceci est très rare en python, car la grammaire python vous oblige à utiliser de style générateur pour parler d'itération; vous ne pouvez pas généraliser facilement. (Ceci est parfois bon, et parfois mauvais.) Vous pouvez probablement trouver des exemples de python rares oùmap(f, *lists)
est une chose raisonnable à faire. L'exemple le plus proche que je peux trouver seraitsumEach = partial(map,sum)
, ce qui est en une ligne qui est très à peu près équivalent à:
def sumEach(myLists):
return [sum(_) for _ in myLists]
- Juste en utilisant une boucle
for
: Vous pouvez aussi bien sûr il suffit d'utiliser une boucle for. Bien que pas aussi élégant du point de vue programmation fonctionnelle, parfois variables non locales rendent le code plus clair dans les langages de programmation impératifs tels que Python, parce que les gens sont très utilisés pour la lecture de code de cette façon. Pour boucles sont également, en général, le plus efficace lorsque vous faites simplement une opération complexe qui ne construit pas une liste comme liste-compréhensions et la carte sont optimisés pour (par exemple sommateur, ou faire un arbre, etc.) - au moins efficace en termes de mémoire (pas nécessairement en termes de temps, où je me attends au pire un facteur constant, sauf des ordures collecte pathologique rare hoquet).
"pythonisme"
Je n'aime pas le mot « pythonique » parce que je ne trouve pas que pythonique est toujours élégant dans mes yeux. Néanmoins, map
et filter
et des fonctions similaires (comme le très utile module itertools
) sont probablement considérés unpythonic en termes de style.
Paresse
En termes d'efficacité, comme la plupart des constructions de programmation fonctionnelle, CARTE PEUT ÊTRE LAZY , et en fait est paresseux en python. Cela signifie que vous pouvez le faire (dans python3 ) et votre ordinateur ne sera pas à court de mémoire et de perdre 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 la liste:
>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #
Notez que la liste compréhensions sont aussi intrinsèquement paresseux, mais python a choisi de les mettre en œuvre comme non paresseux . Néanmoins, python supporte compréhensions liste paresseux sous la forme de générateur exprSESSIONS, comme suit:
>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>
Vous pouvez penser essentiellement de la syntaxe de [...]
en passant dans une expression du générateur au constructeur de liste, comme list(x for x in range(5))
.
exemple artificiel bref
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))})
Liste compréhensions ne sont pas paresseux, donc peut nécessiter plus de mémoire (sauf si vous utilisez compréhensions générateur). Les crochets [...]
font souvent des choses évidentes, surtout quand dans un désordre de parenthèses. D'autre part, on finit parfois par être bavard comme taper [x for x in...
. Tant que vous gardez vos variables iterator courte, la liste compréhensions sont généralement plus claire si vous n'indentez pas votre code. Mais vous pouvez toujours indentez votre code.
print(
{x:x**2 for x in (-y for y in range(5))}
)
ou briser les choses:
rangeNeg5 = (-y for y in range(5))
print(
{x:x**2 for x in rangeNeg5}
)
Comparaison de l'efficacité pour 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 ne comptez pas utiliser toutes vos données, ou ne savent pas à l'avance combien de données dont vous avez besoin, map
dans python3 (et les expressions de générateur python2 ou python3) évitera le calcul de leurs valeurs jusqu'à ce que le dernier moment nécessaire. Habituellement, cela l'emportent généralement sur les frais généraux tout d'utiliser map
. L'inconvénient est que cela est très limité en python par opposition aux langues les plus fonctionnelles. Vous obtenez seulement cet avantage si vous accédez à vos données de gauche à droite « pour », parce que les expressions du générateur python ne peut évaluer l'ordre x[0], x[1], x[2], ...
Cependant, disons que nous avons une fonction pré-faites 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>)
Dans les résultats sont sous la forme AAA / BBB / CCC où A a été réalisée avec un poste de travail Intel vers 2010 avec Python 3.?.?, Et B et C ont été réalisées avec un vers 2013 station de travail AMD avec Python 3.2 .1, avec du matériel extrêmement différents. Le résultat semble être cette carte et la liste compréhensions des performances comparables, ce qui est le plus fortement affecté par d'autres facteurs aléatoires. La seule chose que nous pouvons dire semble être que, curieusement, alors que nous attendons compréhensions liste [...]
de meilleures performances que les expressions du générateur (...)
, map
est également plus efficace que les expressions du générateur (en supposant à nouveau que toutes les valeurs sont évaluées / utilisées).
Il est important de réaliser que ces tests prennent une fonction très simple (la fonction d'identité); mais cela est très bien parce que si la fonction par rapport à d'autres facteurs du programme étaient compliquées, alors les frais généraux de performance serait négligeable. (Il peut encore être intéressant de tester avec d'autres choses simples comme f=lambda x:x+x
)
Si vous êtes habile à l'assemblage python de lecture, vous pouvez utiliser le module dis
pour voir si cela est en fait 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 est préférable d'utiliser la syntaxe [...]
que list(...)
. Malheureusement, la classe map
est un peu opaque pour le démontage, mais nous pouvons faire en raison de notre test de vitesse.
Python 2: Vous devez utiliser map
et filter
au lieu de la liste compréhensions
.
Objectif raison pour laquelle vous devriez les préférer, même si elles ne sont pas « Pythonic » est la suivante:
Ils ont besoin des fonctions / lambdas comme arguments, introduire un nouveau champ .
Je m'y suis mordu par 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 que je lui avais dit:
for x, y in somePoints:
# (several lines of code here)
squared = map(lambda x: x ** 2, numbers)
alors tout aurait été très bien.
Vous pouvez dire que j'étais stupide pour utiliser le même nom de variable dans la même portée.
Je n'étais pas. Le code était bien à l'origine - les deux x
s ne sont pas dans la même portée
.
Ce ne fut qu'après I déplacé le bloc interne à une autre section du code que le problème est venu. (Lire: problème lors de l'entretien, pas de développement), et je ne m'y attendais pas
Oui, si vous ne faites jamais cette erreur puis compréhensions de la liste sont plus élégante.
Mais de son expérience personnelle (et de voir d'autres font la même erreur) Je l'ai vu arriver assez souvent que je pense qu'il est vous ne vaut pas la peine de passer par ces bugs quand se glisser dans votre code.
Conclusion:
Utilisez map
et filter
. Ils empêchent difficiles à diagnostiquer subtils bogues liés à la portée.
Side note:
Ne pas oublier d'envisager d'utiliser imap
et ifilter
(en itertools
) si elles sont appropriées à votre situation!
En fait, map
et liste compréhensions se comportent très différemment dans le langage Python 3. Jetez un coup d'oeil 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 à imprimer la ligne "[1, 4, 9]" deux fois, mais il imprime suivi par "[]" "[1, 4, 9]". 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 un vide.
Dans le Python 2 map
langue renvoie une liste ancienne plaine, tout comme la liste compréhensions font dans les deux langues. Le point crucial est que la valeur de retour de map
en 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érer sur un iterator contrairement à lorsque vous boucle une liste. Voilà pourquoi squares
semble vide dans la dernière ligne de print(list(squares))
.
Pour résumer:
- Lorsque vous traitez avec itérateurs vous devez vous rappeler qu'ils sont stateful et qu'ils mutent que vous les traversent.
- Les listes sont plus prévisibles, car ils ne changent que lorsque vous les muter explicitement; ils sont conteneurs .
- Et un bonus: nombres, des chaînes et tuples sont encore plus prévisibles, car ils ne peuvent pas changer du tout; ils sont valeurs .
Je trouve compréhensions de liste sont généralement plus expressifs de ce que je suis en train de faire que map
- ils ont tous deux obtenir fait, mais l'ancien sauve la charge mentale d'essayer de comprendre ce qui pourrait être une expression lambda
complexe
Il y a aussi une interview là quelque part (je ne peux pas trouver Morisque) où les listes Guido lambda
s et les fonctions fonctionnelles comme la chose qu'il a le plus de regrets à accepter en Python, de sorte que vous pourriez faire l'argument selon lequel ils sont ONU- pythonic en vertu de ce.
Voici un cas possible:
map(lambda op1,op2: op1*op2, list1, list2)
par rapport à:
[op1*op2 for op1,op2 in zip(list1,list2)]
Je devine que le zip () est une surcharge regrettable et inutile que vous devez livrer si vous insistez sur l'utilisation de la liste au lieu compréhensions de la carte. Si quelqu'un clarifie ce que affirmativement ou négativement.
Si vous envisagez d'écrire une asynchrone, parallèle ou code distribué, vous préférerez probablement map
sur une compréhension de liste - comme la plupart asynchrone, parallèle ou paquets distribués fournissent une fonction de map
surcharger le map
de python. Ensuite, en passant la fonction map
appropriée pour le reste de votre code, vous ne pouvez pas avoir à modifier votre numéro de série d'origine pour avoir exécuté en parallèle (etc).
depuis Python 3, map()
est un itérateur, vous besoin de garder à l'esprit ce que vous avez besoin:. un objet iterator ou list
, map()
est plus rapide que la compréhension de la liste que si vous ne l'utilisez la fonction lambda
.
Je vais vous présenter quelques comparaisons de temps.
Python 3.5.2 et CPython
Je l'ai utilisé Jupiter portable et surtout %timeit
intégré commande magique
Mesures : s == == 1000 ms 1000 * 1000 ms = 1000 * 1000 * 1000 ns
Configuration:
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
Fonction lambda
:
%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 y a aussi une telle chose comme l'expression du générateur, voir PEP 0289 . Donc, je pensais que ce serait utile d'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 devez objet list
:
Utiliser la compréhension de la liste si elle est fonction personnalisée, utilisez list(map())
s'il est fonction builtin
Vous n'avez pas besoin objet list
, il vous suffit itérables un:
Utilisez toujours map()
!
Je considère que la façon la plus Pythonic est d'utiliser une compréhension de liste au lieu de map
et filter
. La raison est que la liste compréhensions sont plus claires 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 voyez un, une compréhension ne nécessite pas des expressions lambda
supplémentaires que les besoins de map
. En outre, une compréhension permet aussi facilement le filtrage, alors que map
nécessite filter
pour permettre le filtrage.
J'ai couru un test rapide comparant trois méthodes pour appeler la méthode d'un objet. La différence de temps, dans ce cas, est négligeable et est une question de la fonction en question (voir la réponse de Martelli @ Alex) . Ici, je regardais 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]
Je regardais les listes (stockées dans la vals
variable) des deux entiers (Python int
) et les nombres à virgule flottante (Python de float
) pour augmenter la taille de la liste. La DummyNum
classe fictive suivante est considérée comme:
class DummyNum(object):
"""Dummy class"""
__slots__ = 'n',
def __init__(self, n):
self.n = n
def add(self):
self.n += 5
Plus précisément, le procédé de add
. L'attribut __slots__
est une simple optimisation en Python pour définir la mémoire totale nécessaire par la classe (attributs), ce qui réduit la taille de la mémoire.
Voici les parcelles résultant.
Comme indiqué précédemment, la technique utilisée fait une différence minime et vous devez coder d'une manière qui est plus lisible pour vous, ou dans le cas particulier. Dans ce cas, la compréhension de la liste (technique de map_comprehension
) est le plus rapide pour les deux types d'ajouts dans un objet, en particulier avec des listes plus courtes.
Consultez cette pastebin pour la source utilisée pour tracer la courbe et les données.