Lambda-Funktion im Listenverständnis

149

Warum ist die Ausgabe der folgenden beiden Listenverständnisse unterschiedlich, obwohl fund die lambdaFunktion gleich sind?

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

und

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

Wohlgemerkt, beide type(f)und type(lambda x: x*x)geben den gleichen Typ zurück.

user763191
quelle
[lambda x: x*x for x in range(10)]ist schneller als die erste, da sie keine externe Schleifenfunktion aufruft, f wiederholt.
Riza
@Selinap: ... nein, stattdessen erstellen Sie jedes Mal eine brandneue Funktion durch die Schleife. ... und der Aufwand für das Erstellen dieser neuen Funktion und das Aufrufen ist etwas langsamer (auf meinem System sowieso).
Gerrat
@Gerrat: Auch mit Overhead ist es noch schneller. Aber natürlich [x*x for x in range(10)]ist es besser.
Riza
34
Ich habe gerade hier eingegeben, um Google Foobar Zugang zu erhalten :)
Gal Margalit

Antworten:

267

Der erste erstellt eine einzelne Lambda-Funktion und ruft sie zehnmal auf.

Der zweite ruft die Funktion nicht auf. Es werden 10 verschiedene Lambda-Funktionen erstellt. Es bringt all diese in eine Liste. Um es dem ersten gleichzusetzen, benötigen Sie:

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

Oder noch besser:

[x*x for x in range(10)]
Winston Ewert
quelle
13
Oder map(lambda x: x*x, range(10))was das OP wahrscheinlich überhaupt bedeutete.
Daniel Roseman
Ja, Lambda x: x * x .. (x) scheint ein Grundsatz zu sein.
Staticor
[Lambda x: x * x für x in Bereich (10)] ist im Grunde eine Funktion in Haskell
Moshe Beeri
@ DanielRoseman, oder um genau zu sein, list(map(lambda x: x*x, range(10)))wird Ihnen geben[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
rrlamichhane
108

Diese Frage berührt einen sehr stinkenden Teil der "berühmten" und "offensichtlichen" Python-Syntax - was Vorrang hat, das Lambda oder das For des Listenverständnisses.

Ich glaube nicht, dass der Zweck des OP darin bestand, eine Liste von Quadraten von 0 bis 9 zu erstellen. Wenn dies der Fall wäre, könnten wir noch mehr Lösungen geben:

squares = []
for x in range(10): squares.append(x*x)
  • Dies ist die gute alte Art der imperativen Syntax.

Aber es geht nicht darum. Der Punkt ist W (hy) TF. Ist dieser mehrdeutige Ausdruck so kontraintuitiv? Und ich habe am Ende einen idiotischen Fall für Sie, also entlassen Sie meine Antwort nicht zu früh (ich hatte sie in einem Vorstellungsgespräch).

Das Verständnis des OP ergab also eine Liste von Lambdas:

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

Dies ist natürlich nur 10 verschiedene Kopien der Quadratur - Funktion finden Sie :

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

Beachten Sie die Speicheradressen der Lambdas - sie sind alle unterschiedlich!

Sie könnten natürlich eine "optimalere" (haha) Version dieses Ausdrucks haben:

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

Sehen? 3 mal das gleiche Lambda.

Bitte beachten Sie, dass ich _als forVariable verwendet habe. Es hat nichts mit dem xin der zu tun lambda(es wird lexikalisch überschattet!). Kapiert?

Ich lasse die Diskussion aus, warum die Syntax-Priorität nicht so ist, dass alles bedeutete:

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

Das könnte sein: [[0, 1, 4, ..., 81]]oder [(0, 1, 4, ..., 81)], oder was ich am logischsten finde , dies wäre ein list1-Element - eine generatorRückgabe der Werte. Es ist einfach nicht der Fall, die Sprache funktioniert nicht so.

ABER was, wenn ...

Was ist, wenn Sie die forVariable NICHT überschatten UND in Ihrem lambdas verwenden?

Na dann passiert Mist. Schau dir das an:

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

das heißt natürlich:

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

ABER es bedeutet nicht:

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

Das ist einfach verrückt!

Die Lambdas im Listenverständnis sind ein Abschluss über den Umfang dieses Verständnisses. Ein lexikalischer Abschluss, also beziehen sie sich auf die iVia-Referenz und nicht auf deren Wert, als sie ausgewertet wurden!

Also, dieser Ausdruck:

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

Ist ungefähr gleichbedeutend mit:

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

Ich bin sicher, wir könnten hier mit einem Python-Dekompiler (womit ich zB das disModul meine ) mehr sehen, aber für eine Python-VM-agnostische Diskussion ist dies ausreichend. Soviel zur Frage zum Vorstellungsgespräch.

Wie macht man nun ein listMultiplikator-Lambdas, das sich wirklich mit aufeinanderfolgenden ganzen Zahlen multipliziert? Nun, ähnlich wie bei der akzeptierten Antwort müssen wir die direkte Bindung lösen, iindem wir sie in eine andere einwickeln lambda, die aufgerufen wird im Ausdruck des Listenverständnisses wird :

Vor:

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

Nach dem:

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

(Ich hatte auch die äußere Lambda-Variable = i , aber ich entschied, dass dies die klarere Lösung ist - ich habe sie eingeführt, ydamit wir alle sehen können, welche Hexe welche ist).

Bearbeiten 2019-08-30:

Nach einem Vorschlag von @josoler, der auch in einer Antwort von @sheridp enthalten ist - der Wert des Listenverständnisses "Schleifenvariable" kann in ein Objekt "eingebettet" werden - ist der Schlüssel für den Zugriff zum richtigen Zeitpunkt. Der Abschnitt "Nachher" oben tut dies, indem er es in ein anderes einwickelt lambdaund es sofort mit dem aktuellen Wert von aufruft i. Eine andere Möglichkeit (etwas einfacher zu lesen - es wird kein 'WAT'-Effekt erzeugt) besteht darin, den Wert iinnerhalb eines partialObjekts zu speichern und das "innere" (Original) lambdaals Argument zu verwenden (übergeben vonpartial Objekt am übergeben wird) Zeitpunkt des Anrufs), dh:

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

Großartig, aber es gibt noch eine kleine Wendung für dich! Nehmen wir an, wir wollen es dem Codeleser nicht leichter machen und den Faktor namentlich übergeben (als Schlüsselwortargument anpartial ). Lassen Sie uns etwas umbenennen:

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

Warten Sie ... Wir ändern die Anzahl der Argumente um 1 und wechseln von "zu vielen" zu "zu wenigen"?

Nun, es ist kein echtes WAT, wenn wir coefauf partialdiese Weise übergehen , wird es zu einem Schlüsselwortargument, also muss es nach dem Positionsargument kommen x, wie folgt:

Nach 3:

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

Ich würde die letzte Version dem verschachtelten Lambda vorziehen, aber jedem seine eigene ...

Tomasz Gandor
quelle
22
Das ist eine grausame und ungewöhnliche Frage zum Vorstellungsgespräch.
szeitlin
1
Wenn mein Kollege nicht gefragt hätte, würde ich wahrscheinlich nie nach dieser Antwort suchen
piggybox
8
Beeindruckend. Ich wurde gerade von diesem absurden Verhalten schwer gebissen. Vielen Dank für Ihren Beitrag!
Ameise
1
Hervorragende Antwort. Ich bin gerade auch auf dieses Problem gestoßen. Einerseits weist es auf eine Python-Einschränkung hin, andererseits kann es auch ein Code-Geruchsindikator sein. Ich verwende diese Lösung für ein Spielzeugprojekt, aber es könnte ein Signal für eine Umstrukturierung in einer Produktionsumgebung sein.
Ahota
2
Der Klarheit und Vollständigkeit halber können Sie das letzte Listenverständnis wie [partial(lambda i, x: i * x, i) for i in (1, 2)]
folgt
19

Der große Unterschied besteht darin, dass das erste Beispiel tatsächlich das Lambda aufruft f(x), während das zweite Beispiel dies nicht tut.

Ihr erstes Beispiel entspricht, [(lambda x: x*x)(x) for x in range(10)]während Ihr zweites Beispiel entspricht[f for x in range(10)] .

Gabe
quelle
11

Der erste

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

wird f()für jeden Wert im Bereich ausgeführtf(x) für jeden Wert

der zweite

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

führt das Lambda für jeden Wert in der Liste aus, sodass alle diese Funktionen generiert werden.

zellio
quelle
11

Die Leute gaben gute Antworten, vergaßen aber, den wichtigsten Teil meiner Meinung nach zu erwähnen: Im zweiten Beispiel ist Xdas Listenverständnis NICHT das gleiche wie das Xder lambdaFunktion, sie sind völlig unabhängig. Das zweite Beispiel ist also dasselbe wie:

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

Die internen Iterationen auf range(10) sind nur für die Erstellung von 10 ähnlichen Lambda-Funktionen in einer Liste verantwortlich (10 separate Funktionen, aber völlig ähnlich - Rückgabe der Potenz 2 jedes Eingangs).

Andererseits funktioniert das erste Beispiel völlig anders, da das X der Iterationen mit den Ergebnissen interagiert. Für jede Iteration ist der Wert X*Xso, dass das Ergebnis wäre[0,1,4,9,16,25, 36, 49, 64 ,81]

Tidhar Seifer
quelle
Dies ist ein wichtiger Punkt. Ich habe dich positiv bewertet und in meiner Antwort darauf eingegangen.
Tomasz Gandor
6

Die anderen Antworten sind korrekt. Wenn Sie jedoch versuchen, eine Liste von Funktionen mit jeweils unterschiedlichen Parametern zu erstellen, die später ausgeführt werden können , führt der folgende Code Folgendes aus:

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]

Während das Beispiel erfunden ist, fand ich es nützlich, wenn ich eine Liste von Funktionen haben wollte, die jeweils etwas anderes drucken, dh

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

for i in a:
    i()
Sheridp
quelle