Лямбда-функция в списках

150

Почему вывод следующих двух списочных представлений различен, хотя fи lambdaфункция одинакова?

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

и

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

Имейте в виду, оба type(f)и type(lambda x: x*x)вернуть тот же тип.

user763191
источник
[lambda x: x*x for x in range(10)]быстрее, чем первый, так как он не вызывает функцию внешнего цикла, f повторно.
Риза
@Selinap: ... нет, вместо этого вы создаете совершенно новую функцию, каждый раз повторяя цикл. ... и затраты на создание этой новой функции, а затем вызов немного медленнее (в любом случае, в моей системе).
Джеррат
@Gerrat: Даже с накладными расходами, это все еще быстрее. Но, конечно [x*x for x in range(10)], лучше.
Риза
34
Я только что вошел сюда, чтобы получить доступ к Google Foobar :)
Gal Margalit

Ответы:

268

Первый создает одну лямбда-функцию и вызывает ее десять раз.

Второй не вызывает функцию. Он создает 10 различных лямбда-функций. Это помещает все те в списке. Чтобы сделать его эквивалентным первому, вам нужно:

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

Или еще лучше:

[x*x for x in range(10)]
Уинстон Эверт
источник
13
Или map(lambda x: x*x, range(10)), возможно, именно это имел в виду ОП.
Даниэль Роузман
да, лямбда х: х * х .. (х) кажется принципом.
staticor
[лямбда х: х * х для х в диапазоне (10)] - это в основном функционал в хаскеле
Моше Пивери
@DanielRoseman, или, если быть точным list(map(lambda x: x*x, range(10))), даст вам[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
rrlamichhane
108

Этот вопрос затрагивает очень вонючую часть «известного» и «очевидного» синтаксиса Python - то, что имеет приоритет, лямбда или для понимания списка.

Я не думаю, что целью ОП было создание списка квадратов от 0 до 9. Если бы это было так, мы могли бы дать еще больше решений:

squares = []
for x in range(10): squares.append(x*x)
  • это хороший старый способ императивного синтаксиса.

Но это не главное. Дело в том, что W (hy) TF - это неоднозначное выражение, настолько нелогичное? И в конце у меня для вас идиотский случай, так что не отклоняйте мой ответ слишком рано (он был у меня на собеседовании).

Итак, понимание ОП вернуло список лямбд:

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

Это, конечно, всего 10 различных копий функции возведения в квадрат, см .:

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

Обратите внимание на адреса памяти лямбд - они все разные!

Конечно, вы можете иметь более «оптимальную» (ха-ха) версию этого выражения:

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

Видеть? 3 раза такая же лямбда.

Обратите внимание, что я использовал _в качестве forпеременной. Это не имеет ничего общего с xin lambda(это лексически затенено!). Возьми?

Я опускаю обсуждение, почему приоритет синтаксиса не таков, что все это означало:

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

который может быть: [[0, 1, 4, ..., 81]]или [(0, 1, 4, ..., 81)], или который я считаю наиболее логичным , это будет элемент listиз 1 - generatorвозвращающий значения. Это просто не тот случай, язык так не работает.

Но что, если ...

Что делать, если вы не затеняете forпеременную и не используете ее в своей переменной lambda???

Ну, тогда случается дерьмо. Посмотри на это:

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

это значит конечно:

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

НО это не значит:

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

Это просто безумие!

Лямбды в понимании списка являются закрытием по всему объему этого понимания. Лексическое закрытие, поэтому они относятся к iпо ссылке, а не его значение , когда они были оценены!

Итак, это выражение:

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

Примерно эквивалентно:

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

Я уверен, что мы могли бы увидеть больше здесь, используя декомпилятор Python (я имею в виду, например, disмодуль), но для обсуждения Python-VM-agnostic этого достаточно. Так много для вопроса собеседования.

Теперь, как сделать listиз лямбда-множителей, которые действительно умножаются на последовательные целые числа? Ну, как и в случае с принятым ответом, нам нужно разорвать прямую связь i, завернув ее в другой lambda, вызываемый внутри. выражения понимания списка:

Перед:

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

После:

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

(У меня была и внешняя лямбда-переменная = i, но я решил, что это более ясное решение - я представилy чтобы мы все могли видеть, какая ведьма какая).

Изменить 2019-08-30:

Следуя предложению @josoler, которое также присутствует в ответе @sheridp - значение «петлевой переменной» для понимания списка может быть «встроено» в объект - ключ для доступа к нему в нужное время. Раздел «После» выше делает это, оборачивая его в другой lambdaи вызывая его немедленно с текущим значением i. Другой способ (немного проще для чтения - он не производит эффекта «WAT») - сохранить значение iвнутри partialобъекта, а «внутренний» (исходный) lambdaпринять его в качестве аргумента (передаваемый отpartial объектом в время звонка), т.е.

После 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)

Отлично, но есть еще маленький поворот для вас! Допустим, мы не хотим облегчить чтение кода и передаем фактор по имени (в качестве ключевого аргументаpartial ). Давайте сделаем переименование:

После 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'

Подождите ... Мы меняем число аргументов на 1 и переходим от «слишком много» к «слишком мало»?

Ну, это не настоящий WAT, когда мы переходим coefк partialтаким образом, становится ключевым словом аргумент, поэтому он должен прийти после того , как позиционнаяx аргумента, например так:

После 3:

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

Я бы предпочел последнюю версию над вложенной лямбдой, но каждому свою ...

Томаш Гандор
источник
22
это жестокий и необычный вопрос собеседования.
szeitlin
1
Если бы мой коллега не спросил, я, вероятно, никогда бы не стал искать этот ответ
piggybox
8
Вот это да. Меня просто укусило это абсурдное поведение. Спасибо за ваше сообщение!
Муравей
1
Отличный ответ. Я тоже столкнулся с этой проблемой. С одной стороны, это указывает на ограничение Python, но с другой стороны, это также может быть индикатор запаха кода. Я использую это решение для игрушечного проекта, но это может быть сигналом к ​​реструктуризации в производственной среде.
Ахота
2
Для ясности и полноты вы можете написать последнее понимание списка следующим образом:[partial(lambda i, x: i * x, i) for i in (1, 2)]
josoler
19

Большая разница в том, что первый пример фактически вызывает лямбду f(x), а второй - нет.

Ваш первый пример эквивалентен, [(lambda x: x*x)(x) for x in range(10)]тогда как ваш второй пример эквивалентен [f for x in range(10)].

Гейб
источник
11

Первый

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

работает f()для каждого значения в диапазоне, так что f(x)для каждого значения

второй

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

запускает лямбду для каждого значения в списке, поэтому он генерирует все эти функции.

zellio
источник
11

Люди дали хорошие ответы , но забыл упомянуть самую важную роль , на мой взгляд: Во втором примере Xв списке понимание не то же самое , как Xв lambdaфункции, они совершенно не связаны. Итак, второй пример на самом деле такой же, как:

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

Внутренние итерации на range(10) отвечают только за создание 10 похожих лямбда-функций в списке (10 отдельных функций, но абсолютно одинаковых - возвращая степень 2 каждого входа).

С другой стороны, первый пример работает совершенно иначе, потому что X итераций DO взаимодействует с результатами, для каждой итерации значение X*Xтак, что результат будет[0,1,4,9,16,25, 36, 49, 64 ,81]

Тидхар Сейфер
источник
Это важный момент. Я проголосовал за вас и подробно остановился на этом в своем ответе.
Томаш Гандор
6

Другие ответы верны, но если вы пытаетесь составить список функций, каждая из которых имеет свой параметр, который может быть выполнен позже , следующий код сделает это:

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]

Хотя пример придуман, я нашел его полезным, когда мне нужен список функций, каждая из которых печатает что-то свое, т.е.

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

for i in a:
    i()
sheridp
источник