Fonction Lambda dans les compréhensions de liste

150

Pourquoi la sortie des deux compréhensions de liste suivantes est-elle différente, même si fet la lambdafonction est la même?

f = lambda x: x*x
[f(x) for x in range(10)]

et

[lambda x: x*x for x in range(10)]

Attention, les deux type(f)et type(lambda x: x*x)renvoyez le même type.

user763191
la source
[lambda x: x*x for x in range(10)]est plus rapide que le premier, car il n'appelle pas de fonction de boucle extérieure, f à plusieurs reprises.
riza
@Selinap: ... non, au lieu de cela, vous créez une nouvelle fonction de marque à chaque fois à travers la boucle. ... et la surcharge de création de cette nouvelle fonction, puis l'appel est un peu plus lent (sur mon système en tout cas).
Gerrat
@Gerrat: Même avec les frais généraux, c'est toujours plus rapide. Mais, bien sûr, [x*x for x in range(10)]c'est mieux.
riza
34
Je viens d'entrer ici pour accéder à google foobar :)
Gal Margalit

Réponses:

268

Le premier crée une seule fonction lambda et l'appelle dix fois.

Le second n'appelle pas la fonction. Il crée 10 fonctions lambda différentes. Il met tout cela dans une liste. Pour le rendre équivalent au premier dont vous avez besoin:

[(lambda x: x*x)(x) for x in range(10)]

Ou mieux encore:

[x*x for x in range(10)]
Winston Ewert
la source
13
Ou map(lambda x: x*x, range(10)), ce qui était probablement ce que le PO signifiait en premier lieu.
Daniel Roseman
ouais, lambda x: x * x .. (x) semble principe.
staticor
[lambda x: x * x pour x dans la plage (10)] est essentiellement un fonctur dans haskell
moshe beeri
@DanielRoseman, ou pour être précis list(map(lambda x: x*x, range(10)))vous donnera[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
rrlamichhane
108

Cette question touche une partie très puante de la syntaxe Python «célèbre» et «évidente» - ce qui prime, le lambda, ou le pour de la compréhension de liste.

Je ne pense pas que le but du PO était de générer une liste de carrés de 0 à 9. Si tel était le cas, nous pourrions donner encore plus de solutions:

squares = []
for x in range(10): squares.append(x*x)
  • c'est la bonne vieille manière de la syntaxe impérative.

Mais ce n'est pas le but. Le point est W (hy) TF est-ce que cette expression ambiguë est si contre-intuitive? Et j'ai un cas idiot pour vous à la fin, alors ne rejetez pas ma réponse trop tôt (je l'ai eue lors d'un entretien d'embauche).

Ainsi, la compréhension de l'OP a renvoyé une liste de lambdas:

[(lambda x: x*x) for x in range(10)]

Il ne s'agit bien sûr que de 10 copies différentes de la fonction de quadrillage, voir:

>>> [lambda x: x*x for _ in range(3)]
[<function <lambda> at 0x00000000023AD438>, <function <lambda> at 0x00000000023AD4A8>, <function <lambda> at 0x00000000023AD3C8>]

Notez les adresses mémoire des lambdas - elles sont toutes différentes!

Vous pourriez bien sûr avoir une version plus "optimale" (haha) de cette expression:

>>> [lambda x: x*x] * 3
[<function <lambda> at 0x00000000023AD2E8>, <function <lambda> at 0x00000000023AD2E8>, <function <lambda> at 0x00000000023AD2E8>]

Voir? 3 fois le même lambda.

Veuillez noter que j'ai utilisé _comme forvariable. Cela n'a rien à voir avec le xdans le lambda(il est éclipsé lexicalement!). Tu piges?

Je laisse de côté la discussion, pourquoi la priorité de syntaxe n'est pas ainsi, que tout cela signifiait:

[lambda x: (x*x for x in range(10))]

qui pourrait être:, [[0, 1, 4, ..., 81]]ou [(0, 1, 4, ..., 81)], ou ce que je trouve le plus logique , ce serait un listélément de 1 - un generatorrenvoyant les valeurs. Ce n'est tout simplement pas le cas, la langue ne fonctionne pas de cette façon.

MAIS quoi, si ...

Et si vous NE PAS éclipser la forvariable ET l'utiliser dans vos lambdas ???

Eh bien, alors la merde arrive. Regarde ça:

[lambda x: x * i for i in range(4)]

cela signifie bien sûr:

[(lambda x: x * i) for i in range(4)]

MAIS cela ne veut pas dire:

[(lambda x: x * 0), (lambda x: x * 1), ... (lambda x: x * 3)]

C'est juste fou!

Les lambdas dans la compréhension de liste sont une clôture sur la portée de cette compréhension. Une fermeture lexicale , donc ils font référence à la iréférence via, et non à sa valeur lors de leur évaluation!

Donc, cette expression:

[(lambda x: x * i) for i in range(4)]

EST à peu près ÉQUIVALENT à:

[(lambda x: x * 3), (lambda x: x * 3), ... (lambda x: x * 3)]

Je suis sûr que nous pourrions en voir plus ici en utilisant un décompilateur python (je veux dire par exemple le dismodule), mais pour une discussion indépendante de Python-VM, cela suffit. Voilà pour la question de l'entretien d'embauche.

Maintenant, comment faire un listmultiplicateur lambdas, qui se multiplie vraiment par des entiers consécutifs? Eh bien, de la même manière que la réponse acceptée, nous devons rompre le lien direct ien l'enveloppant dans une autre lambda, qui est appelée dans l'expression de compréhension de liste:

Avant:

>>> a = [(lambda x: x * i) for i in (1, 2)]
>>> a[1](1)
2
>>> a[0](1)
2

Après:

>>> a = [(lambda y: (lambda x: y * x))(i) for i in (1, 2)]
>>> a[1](1)
2
>>> a[0](1)
1

(J'avais également la variable lambda externe = i, mais j'ai décidé que c'était la solution la plus claire - j'ai introduit ypour que nous puissions tous voir quelle sorcière est quelle).

Modifier le 30/08/2019:

Suite à une suggestion de @josoler, qui est également présente dans une réponse de @sheridp - la valeur de la "variable de boucle" de compréhension de liste peut être "intégrée" à l'intérieur d'un objet - la clé est d'y accéder au bon moment. La section «Après» ci-dessus le fait en l'enveloppant dans un autre lambdaet en l'appelant immédiatement avec la valeur actuelle de i. Une autre façon (un peu plus facile à lire - cela ne produit pas d'effet 'WAT') est de stocker la valeur de l' iintérieur d'un partialobjet, et de faire en sorte que l '"intérieur" (original) le lambdaprenne comme argument (passé fourni par l' partialobjet au heure de l'appel), c'est-à-dire:

Après 2:

>>> from functools import partial
>>> a = [partial(lambda y, x: y * x, i) for i in (1, 2)]
>>> a[0](2), a[1](2)
(2, 4)

Génial, mais il y a encore un petit rebondissement pour vous! Disons que nous ne voulons pas faciliter la tâche du lecteur de code et transmettre le facteur par son nom (comme argument de mot-clé à partial). Faisons un peu de changement de nom:

Après 2,5:

>>> a = [partial(lambda coef, x: coef * x, coef=i) for i in (1, 2)]
>>> a[0](1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() got multiple values for argument 'coef'

WAT?

>>> a[0]()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 1 required positional argument: 'x'

Attendez ... Nous changeons le nombre d'arguments par 1, et passons de "trop" à "trop ​​peu"?

Eh bien, ce n'est pas un vrai WAT, quand on passe coefà partialde cette façon, cela devient un argument mot-clé, donc il doit venir après l' xargument positionnel , comme ceci:

Après 3:

>>> a = [partial(lambda x, coef: coef * x, coef=i) for i in (1, 2)]
>>> a[0](2), a[1](2)
(2, 4)

Je préférerais la dernière version au lambda imbriqué, mais à chacun leur propre ...

Tomasz Gandor
la source
22
c'est une question d'entrevue d'emploi cruelle et inhabituelle.
szeitlin
1
Si mon collègue ne le demandait pas, je ne chercherais probablement jamais cette réponse
tirelire
8
Sensationnel. Je viens de me faire mordre par ce comportement absurde. Merci pour votre commentaire!
Ant
1
Excellente réponse. Je viens de rencontrer ce problème aussi. D'une part, cela indique une limitation Python, mais d'autre part, cela peut également être un indicateur d'odeur de code. J'utilise cette solution pour un projet de jouet, mais cela pourrait être un signal de restructuration dans un environnement de production.
ahota le
2
Par souci de clarté et d'exhaustivité, vous pouvez écrire la dernière compréhension de la liste comme:[partial(lambda i, x: i * x, i) for i in (1, 2)]
josoler
19

La grande différence est que le premier exemple appelle en fait le lambda f(x), contrairement au deuxième exemple.

Votre premier exemple est équivalent à [(lambda x: x*x)(x) for x in range(10)]tandis que votre deuxième exemple est équivalent à [f for x in range(10)].

Gabe
la source
11

Le premier

f = lambda x: x*x
[f(x) for x in range(10)]

s'exécute f()pour chaque valeur de la plage, il le fait f(x)pour chaque valeur

le deuxième

[lambda x: x*x for x in range(10)]

exécute le lambda pour chaque valeur de la liste, donc il génère toutes ces fonctions.

zellio
la source
11

Les gens ont donné de bonnes réponses mais ont oublié de mentionner la partie la plus importante à mon avis: dans le deuxième exemple, la Xcompréhension de la liste n'est PAS la même que celle Xde la lambdafonction, elles sont totalement indépendantes. Donc, le deuxième exemple est en fait le même que:

[Lambda X: X*X for I in range(10)]

Les itérations internes sur range(10)ne sont responsables que de la création de 10 fonctions lambda similaires dans une liste (10 fonctions distinctes mais totalement similaires - renvoyant la puissance 2 de chaque entrée).

D'autre part, le premier exemple fonctionne totalement différemment, car le X des itérations DO interagit avec les résultats, pour chaque itération la valeur est X*Xdonc le résultat serait[0,1,4,9,16,25, 36, 49, 64 ,81]

Tidhar seifer
la source
C'est un point important. Je vous ai voté pour et je l'ai développé dans ma réponse.
Tomasz Gandor
6

Les autres réponses sont correctes, mais si vous essayez de faire une liste de fonctions, chacune avec un paramètre différent, qui peuvent être exécutées plus tard , le code suivant le fera:

import functools
a = [functools.partial(lambda x: x*x, x) for x in range(10)]

b = []
for i in a:
    b.append(i())

In [26]: b
Out[26]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Bien que l'exemple soit artificiel, je l'ai trouvé utile lorsque je voulais une liste de fonctions qui impriment chacune quelque chose de différent, c'est-à-dire

import functools
a = [functools.partial(lambda x: print(x), x) for x in range(10)]

for i in a:
    i()
Sheridp
la source