Funkcja lambda w wyrażeniach listowych

149

Dlaczego dane wyjściowe następujących dwóch składanych list są różne, mimo że funkcja fi lambdafunkcja są takie same?

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

i

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

Pamiętaj o obu type(f)i type(lambda x: x*x)zwróć ten sam typ.

user763191
źródło
[lambda x: x*x for x in range(10)]jest szybszy niż pierwszy, ponieważ nie wywołuje wielokrotnie funkcji pętli zewnętrznej f.
riza
@Selinap: ... nie, zamiast tego tworzysz nową funkcję za każdym razem w pętli. ... i narzut tworzenia tej nowej funkcji, a następnie wywoływanie jest trochę wolniejsze (w każdym razie w moim systemie).
Gerrat
@Gerrat: Nawet z nad głową jest jeszcze szybszy. Ale oczywiście [x*x for x in range(10)]jest lepiej.
riza
34
Właśnie wszedłem tutaj, aby uzyskać dostęp do Google foobar :)
Gal Margalit

Odpowiedzi:

267

Pierwsza tworzy pojedynczą funkcję lambda i wywołuje ją dziesięć razy.

Drugi nie wywołuje funkcji. Tworzy 10 różnych funkcji lambda. Umieszcza je wszystkie na liście. Aby uczynić go odpowiednikiem pierwszego, którego potrzebujesz:

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

Lub jeszcze lepiej:

[x*x for x in range(10)]
Winston Ewert
źródło
13
Albo map(lambda x: x*x, range(10)), co prawdopodobnie było tym, co miał na myśli PO.
Daniel Roseman
tak, lambda x: x * x .. (x) wydaje się założeniem.
staticor
[lambda x: x * x for x in range (10)] jest w zasadzie funktorem w haskell
moshe beeri
@DanielRoseman, a dokładniej list(map(lambda x: x*x, range(10)))da ci[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
rrlamichhane
108

To pytanie dotyka bardzo śmierdzącej części „słynnej” i „oczywistej” składni Pythona - tego, co ma pierwszeństwo, lambda, czyli for of list compilation.

Nie sądzę, aby celem PO było wygenerowanie listy kwadratów od 0 do 9. Gdyby tak było, moglibyśmy podać jeszcze więcej rozwiązań:

squares = []
for x in range(10): squares.append(x*x)
  • to jest stary dobry sposób imperatywnej składni.

Ale nie o to chodzi. Chodzi o to, że W (hy) TF czy to niejednoznaczne wyrażenie jest tak sprzeczne z intuicją? I na koniec mam dla ciebie idiotyczną sprawę, więc nie odrzucaj mojej odpowiedzi zbyt wcześnie (miałem ją na rozmowie o pracę).

Tak więc zrozumienie OP zwróciło listę lambd:

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

To oczywiście tylko 10 różnych kopii funkcji do kwadratu, patrz:

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

Zwróć uwagę na adresy pamięci lambd - wszystkie są różne!

Oczywiście możesz mieć bardziej „optymalną” (haha) wersję tego wyrażenia:

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

Widzieć? 3 razy ta sama lambda.

Zwróć uwagę, że użyłem _jako forzmiennej. Nie ma to nic wspólnego z xw lambda(jest leksykalnie przyćmione!). Zdobyć?

Pomijam dyskusję, dlaczego pierwszeństwo składni nie jest takie, że to wszystko oznaczało:

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

który mógłby być:, [[0, 1, 4, ..., 81]]lub [(0, 1, 4, ..., 81)], lub co wydaje mi się najbardziej logiczne , byłby to listelement 1 - generatorzwracający wartości. Po prostu tak nie jest, język nie działa w ten sposób.

ALE co, jeśli ...

A co jeśli NIE przyćmisz forzmiennej ORAZ użyjesz jej w swoim lambdas ???

Cóż, wtedy dzieje się bzdury. Spójrz na to:

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

oznacza to oczywiście:

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

ALE NIE OZNACZA:

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

To jest po prostu szalone!

Lambdy w zrozumieniu list są zamknięciem zakresu tego rozumienia. Leksykalny zamknięcie, więc odnoszą się do ipoprzez odniesienie, a nie jej wartość, gdy oceniano!

A więc to wyrażenie:

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

JEST Z grubsza RÓWNOWAŻNA do:

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

Jestem pewien, że moglibyśmy zobaczyć więcej tutaj, używając dekompilatora Pythona (przez co mam na myśli np. disModuł), ale dla dyskusji Python-VM-agnostic to wystarczy. To tyle, jeśli chodzi o pytanie o pracę.

A teraz, jak zrobić a listz mnożników lambd, które naprawdę mnożą się przez kolejne liczby całkowite? Cóż, podobnie jak zaakceptowana odpowiedź, musimy zerwać bezpośrednie powiązanie i, zawijając je w inny lambda, który jest wywoływany wewnątrz wyrażenia rozumienia listy:

Przed:

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

Po:

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

(Miałem również zewnętrzną zmienną lambda = i, ale zdecydowałem, że to jest jaśniejsze rozwiązanie - wprowadziłem y, abyśmy wszyscy mogli zobaczyć, która wiedźma jest którą).

Edycja 30.08.2019:

Zgodnie z sugestią @josoler, która jest również obecna w odpowiedzi @sheridp - wartość „zmiennej pętli” rozumienia listy może być „osadzona” wewnątrz obiektu - kluczem jest, aby uzyskać do niej dostęp w odpowiednim czasie. Sekcja „After” powyżej robi to, zawijając ją w inną lambdai wywołując ją natychmiast z bieżącą wartością i. Innym sposobem (nieco łatwiejszym do odczytania - nie wywołuje efektu „WAT”) jest przechowywanie wartości iwewnątrz partialobiektu i sprawienie, aby „wewnętrzna” (oryginał) lambdawzięła ją jako argument (przekazany przez partialobiekt w czas połączenia), tj .:

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

Świetnie, ale wciąż jest dla Ciebie mały zwrot akcji! Powiedzmy, że nie chcemy ułatwić czytnika kodu i przekazać czynnik według nazwy (jako argument słowa kluczowego do partial). Zróbmy trochę zmiany nazwy:

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

Czekaj ... Zmieniamy liczbę argumentów o 1 i przechodzimy od „za dużo” do „za mało”?

Cóż, to nie jest prawdziwy WAT, gdy mijamy coefsię partialw ten sposób, staje się argumentem kluczowym, więc musi przyjść po pozycyjnym xargumentu, tak jak poniżej:

Po 3:

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

Wolałbym ostatnią wersję od zagnieżdżonej lambdy, ale dla każdej ich własnej ...

Tomasz Gandor
źródło
22
to okrutne i niezwykłe pytanie na rozmowę kwalifikacyjną.
szeitlin,
1
Gdyby mój kolega nie zapytał, prawdopodobnie nigdy bym nie szukał tej odpowiedzi
piggybox
8
Łał. Właśnie to absurdalne zachowanie mnie ugryzło. Dziękuję za Twój post!
Ant
1
Doskonała odpowiedź. Właśnie natknąłem się na ten problem. Z jednej strony wskazuje na ograniczenie Pythona, ale z drugiej strony może to być również wskaźnik zapachu kodu. Używam tego rozwiązania do projektu zabawki, ale może to być sygnał do restrukturyzacji w środowisku produkcyjnym.
ahota
2
Dla jasności i kompletności możesz napisać ostatnie rozumienie listy, na przykład:[partial(lambda i, x: i * x, i) for i in (1, 2)]
josoler
19

Duża różnica polega na tym, że pierwszy przykład faktycznie wywołuje lambdę f(x), a drugi nie.

Twój pierwszy przykład jest równoważny, [(lambda x: x*x)(x) for x in range(10)]podczas gdy twój drugi przykład jest równoważny [f for x in range(10)].

Gabe
źródło
11

Pierwszy

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

działa f()dla każdej wartości w zakresie, więc działa f(x)dla każdej wartości

Drugie

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

uruchamia lambdę dla każdej wartości z listy, więc generuje wszystkie te funkcje.

zellio
źródło
11

Ludzie dał dobre odpowiedzi, ale zapomniałem wspomnieć najważniejszą rolę w moim zdaniem: W drugim przykładzie Xz listy ze zrozumieniem nie jest taka sama jak Xz lambdafunkcji, są całkowicie niezależne. Zatem drugi przykład jest w rzeczywistości taki sam, jak:

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

Wewnętrzne iteracje range(10)są odpowiedzialne tylko za utworzenie 10 podobnych funkcji lambda na liście (10 oddzielnych funkcji, ale całkowicie podobnych - zwracanie potęgi 2 z każdego wejścia).

Z drugiej strony, pierwszy przykład działa zupełnie inaczej, ponieważ X iteracji DZIAŁA z wynikami, dla każdej iteracji wartość jest X*Xtaka, że ​​wynik byłby[0,1,4,9,16,25, 36, 49, 64 ,81]

Tidhar seifer
źródło
To jest ważna kwestia. Głosowałem za tobą i rozwinąłem to w mojej odpowiedzi.
Tomasz Gandor
6

Pozostałe odpowiedzi są poprawne, ale jeśli próbujesz utworzyć listę funkcji, z których każda ma inny parametr, które można wykonać później , zrobi to następujący kod:

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]

Chociaż przykład jest wymyślony, uznałem go za przydatny, gdy chciałem uzyskać listę funkcji, z których każda drukuje coś innego, tj

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

for i in a:
    i()
sheridp
źródło