Deep Engineering
Средний·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·20 МИН

lambda: то же самое, что def, кроме двух вещей — и знаменитая ошибка с циклом не из их числа

У lambda и def один тип, один байт-код тела и неотличимое время вызова. Разница в имени и в том, что тело — выражение. А знаменитая ошибка с циклом к lambda вообще не относится: обычный def ломается точно так же.

Полное техническое изложение

TL;DR

lambda — это обычная функция, записанная выражением. Она нужна там, где функция маленькая и требуется прямо в месте вызова, — например аргументом в sorted, — и заводить для неё отдельное имя значит мешать читателю, а не помогать ему. Объект при этом получается тот же, что у def: один тип, одинаковый байт-код тела.

Отсюда главное следствие: выбор между lambda и def — про читаемость, а не про поведение. Различия лежат в грамматике и метаданных: тело лямбды обязано быть выражением, а имя у неё синтетическое — <lambda> вместо настоящего, и видно это в трассировке. Знаменитая ошибка с циклом — [lambda: i for i in range(3)] даёт [2, 2, 2]к lambda не относится: обычный def в цикле ломается так же, в FAQ это сказано прямо, а причина в том, что три функции ссылаются на одну ячейку (проверено — ячейка ровно одна, и в ней лежит 2).

Дальше — то, что отличает знающего от читавшего. Время вызова у def и lambda неотличимо: 22,7 нс против 22,7 нс (3.13.7). Аннотировать лямбду нельзя, и запрещает это не правило «только выражения», а сама грамматика (PEP 3107). Цена в key=: lambda и def неотличимы (7,97 против 7,98 мкс), а operator.itemgetter быстрее обоих в полтора раза на этом замере. Выбирать надо не между lambda и def.

Порог входа
Перед уроком достаточно понимать
  • функция — такой же объект, как число или строка: её можно положить в переменную и передать в другую функцию аргументом;
  • выражение вычисляется и даёт значение, а инструкция что-то делает: x + 1 — выражение, return x — инструкция;
  • sorted(data, key=…) — обычный вызов, которому передают функцию.
Заранее знать не нужно
  • замыкание, ячейка, __closure__, позднее связывание;
  • operator.itemgetter, морж :=, аннотации типов и байт-код.

База: обычная функция, записанная выражением

Функцию в Python можно передать аргументом — и стандартная библиотека этим пользуется постоянно. sorted, min, max, filter принимают функцию, потому что им нужен не готовый алгоритм, а правило: по какому значению сравнивать, что считать подходящим.

Обычно такую функцию сначала объявляют, а потом передают:

PYTHON
def second(pair):
    return pair[1]
 
rows.sort(key=second)

lambda — способ записать ровно ту же функцию выражением и поставить её прямо туда, где она нужна:

PYTHON
rows.sort(key=lambda pair: pair[1])

Обе записи дают функцию, обе делают одно и то же. Разница в том, что во второй у функции нет отдельного имени и отдельного места в файле.

Отсюда и ответ на вопрос «когда стоит писать так». Когда функция маленькая, нужна ровно в одном месте — в самом вызове, — и отдельное имя для неё скорее мешает: заставляет читателя отвести глаза, найти определение и вернуться обратно, чтобы узнать то, что помещалось в полстроки.

И обратное правило из того же соображения: как только имя нужно — функция вызывается из нескольких мест, ей нужна аннотация типа, её имя должно быть видно в трассировке при падении, — пишут def. Это же говорит и PEP 8, возражая против записи f = lambda ...: в ней имя вроде бы есть, а в трассировке его нет.

Этого уже достаточно, чтобы ответить на базовый вопрос собеседования: lambda — та же функция, записанная выражением, и уместна она там, где отдельное имя ухудшает читаемость. Всё дальнейшее — про то, чем именно lambda отличается от def (отличий немного, и скорости среди них нет), про знаменитую ошибку с циклом, которая к lambda не относится, и про то, что на самом деле показывают замеры.

Механизм 1: один тип, один байт-код, одно время

контракт языкаГарантия языка: lambda даёт объект того же типа function, что и def. Различия — в грамматике и метаданных.

Начнём с того, что проверяется тремя строками.

PYTHON
def named(x):
    return x + 1
 
anon = lambda x: x + 1

type(named) is type(anon)True, оба function. Байт-код тела совпадает инструкция в инструкцию:

RESUME  LOAD_FAST  LOAD_CONST  BINARY_OP  RETURN_VALUE

Время вызова — 22,7 нс у def против 22,7 нс у lambda на 3.13.7. Разница меньше разброса между запусками; в повторных прогонах порядок меняется местами. Утверждение здесь именно такое: измерять нечего.

Справочник описывает lambda одной строкой грамматики и говорит, что выражение lambda arguments: expression yields a function object (порождает объект-функцию) — тот же объект, что делает def.

Механизм 2: отличие первое — имя

__name__ у лямбды — <lambda>, и это видно там, где важнее всего. Две функции, которые падают одинаково:

PYTHON
def boom_named(x): return int(x)
boom_anon = lambda x: int(x)
 
boom_named("тест")   # ValueError — и то же самое у boom_anon

В трассировке они выглядят по-разному:

последний кадр (def):    boom_named
последний кадр (lambda): <lambda>

Отсюда рекомендация PEP 8, у которой есть причина, а не вкус: Always use a def statement instead of an assignment statement that binds a lambda expression directly to an identifier (Всегда используйте инструкцию def вместо присваивания, связывающего лямбда-выражение прямо с именем) — потому что the name of the resulting function object is specifically 'f' instead of the generic '<lambda>'. This is more useful for tracebacks (имя получившегося объекта-функции — именно 'f', а не безликое '<lambda>'. Это полезнее для трассировок).

То есть запрет касается только случая f = lambda .... Передать лямбду аргументом — sorted(data, key=lambda x: x[1]) — PEP 8 не запрещает: там имени и не предполагалось.

Механизм 3: отличие второе — тело обязано быть выражением

Справочник: lambda forms can only contain expressions, not statements (формы lambda могут содержать только выражения, но не инструкции). Проверено компиляцией, что именно не проходит:

записьрезультат
lambda x: (x := 1)компилируется — морж это выражение
lambda x: x = 1SyntaxError
lambda x: return xSyntaxError
lambda x: raise ValueErrorSyntaxError
lambda x: assert xSyntaxError
lambda x -> int: xSyntaxError — аннотация возврата не разрешена

Последняя строка — про другое: аннотацию запрещает не правило «только выражения», а грамматика самой лямбды, и PEP 3107 говорит об этом прямо. На практике она важнее остальных: лямбду нельзя аннотировать. Если у функции должен быть тип — это def, и не из вкусовых соображений.

Механизм 4: ошибка, которая не падает

А теперь код, который не падает, ничего не предупреждает — и возвращает не то.

PYTHON
fns = [lambda: i for i in range(3)]
[f() for f in fns]        # [2, 2, 2], а не [0, 1, 2]

Это не про lambda. FAQ заканчивает объяснение фразой, которую обычно не дочитывают: Note that this behaviour is not peculiar to lambdas, but applies to regular functions too (Заметьте, что это поведение свойственно не только лямбдам, но и обычным функциям). Проверено запуском — цикл с обычным def даёт те же [2, 2, 2]:

PYTHON
defs = []
for i in range(3):
    def make():
        return i
    defs.append(make)
[f() for f in defs]       # [2, 2, 2] — lambda здесь ни при чём

Причина видна в памяти: у трёх функций одна ячейка замыкания. Ячейка (cell) — отдельный объект, который CPython заводит для переменной, если на неё смотрит не только та функция, где она создана; ссылки на такие объекты функция держит в __closure__. Проверено — множество id ячеек содержит ровно один элемент, и в ячейке лежит 2.

Так это выглядит не в учебном примере: три проверки, собранные в цикле, получают один порог — последний. [lambda v: v > t for t in (10, 100, 1000)] пропускает 3 значения вместо 6, и ни одного исключения при этом нет.

Обход — аргумент по умолчанию, потому что он вычисляется в момент создания функции:

PYTHON
fixed = [lambda i=i: i for i in range(3)]   # [0, 1, 2]

У таких функций __closure__ равен None: замыкания нет вовсе, значение лежит в самой функции.

Чем включение отличается от цикла

Здесь есть деталь, которой нет в FAQ: он показывает пример на for-цикле, а у включения переменная ведёт себя иначе — ячейка одна и там и там, но живёт она по-разному.

переменная видна послесмена её значения меняет результат
for x in range(3): …да, x == 2да: x = 100 → все три вернут 100
[… for y in range(3)]нетнечему меняться — снаружи y нет

То есть в цикле функции держат ссылку на переменную, которая живёт дальше и которую можно испортить позже. У включения своя область видимости, наружу ничего не выходит — но ячейка у трёх функций всё равно одна, и результат одинаковый.

Глубже: сколько это стоит на самом деле

наблюдение замераbench/lambda/cost.py, CPython 3.11–3.14. Числа сняты на одной машине; содержательно отсутствие разницы, а не абсолют.

Три способа достать один и тот же ключ; в таблице — время одной сортировки списка из 200 кортежей, лучшее из шестидесяти чередующихся кругов по 2 000 сортировок.

способ3.13.73.14.7
key=lambda x: x[1]7,97 мкс7,67 мкс
key=named (тот же def)7,98 мкс7,71 мкс
key=operator.itemgetter(1)5,30 мкс5,40 мкс

Колонки читаются по отдельности: сборки, на которых сняты эти числа, различаются не только версией языка. Смотреть надо на строки внутри одной колонки.

Первые две строки неразличимы — и это ожидаемо, объект в обоих случаях одного и того же типа. А itemgetter быстрее обоих в полтора раза, и эта разница между запусками устойчива.

Причина не в синтаксисе: itemgetter — вызываемый объект, написанный на C, и на каждый элемент не создаётся кадр интерпретатора. То же верно для attrgetter и methodcaller.

У «полутора раз» есть граница, и назвать её важнее самого числа. Замерялась одна операция — сортировка списка из 200 кортежей по одному элементу; в практике этого же урока, на других данных и другой машине, тот же переход дал больше двух раз. Переносится не множитель, а направление: выигрыш даёт исчезновение вызова кода на Python, и величина его зависит от того, какую долю времени этот вызов занимал. На сортировке из десяти элементов не изменится ничего.

Поэтому практический вывод пишется от границы: спор «lambda или def» не стоит ничего ни в ту, ни в другую сторону — измеримой разницы между ними в замере нет. Если сортировка действительно горячая, смотреть надо в сторону operator, и смотреть замером на своих данных, а не выбирать между двумя способами написать одно и то же.

Глубже: история версий

ВерсияИзменениеЧто это значит для кода
3.0PEP 3107 даёт аннотации def и не даёт их lambda — и это записанное решение, а не недосмотр: Lambda's syntax does not support annotations (синтаксис lambda не поддерживает аннотации). С тех пор функция, которой нужен тип, обязана быть def, и это проверяется компиляцией, а не соглашением.
3.8PEP 572: := — именно выражение, а не инструкция, поэтому внутри lambda он работает. Это единственный способ что-то «присвоить» в теле лямбды, и запись lambda x: (y := x) компилируется, в отличие от lambda x: y = x.
3.12Включения встроены в вызывающий код (PEP 709), но собственная область видимости у них осталась: переменная включения по-прежнему не видна снаружи — проверено на 3.11, 3.12, 3.13 и 3.14, результат одинаковый.

Как отвечать на собеседовании

Короткий ответ: lambda — та же функция, записанная выражением. Объект получается того же типа, что и у def, с тем же байт-кодом тела; пишут её там, где функция нужна прямо в месте вызова и отдельное имя ухудшило бы читаемость.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Первое — назвать различия по существу. Их два, и оба в грамматике и метаданных: имя (<lambda> вместо настоящего — оно и видно в трассировке) и то, что тело обязано быть выражением. Третье, грамматическое: лямбду нельзя аннотировать, и запрещает это грамматика, а не правило «только выражения». Отсюда же рамка PEP 8: он возражает не против lambda, а против записи f = lambda ..., потому что она забирает имя из трассировки.

Второе — ловушка, которую ждут: знаменитое [lambda: i for i in range(3)], дающее [2, 2, 2], к lambda отношения не имеет — обычный def в цикле ломается так же, и FAQ говорит это прямо. Причина в том, что три функции ссылаются на одну ячейку.

Третье — если разговор ушёл в скорость, назвать настоящую развилку: lambda и def в key= неотличимы (7,97 против 7,98 мкс), а operator.itemgetter в этом замере быстрее обоих в полтора раза. Множитель — свойство этого замера, переносится направление: выигрыш даёт исчезновение вызова кода на Python.

Дальше спросят

Спросят дальше

Если это тот же def, зачем lambda вообще нужна?

Короткий ответ

Отличий ровно два, и оба не про скорость: у неё нет своего имени и её тело — выражение, а не набор инструкций. Тип объекта, байт-код и время у lambda и def одинаковые.

Спросят дальше

Знаменитая ловушка с lambda в цикле — это про lambda?

Короткий ответ

Нет, это про замыкания: захвачена переменная, а не значение, и def в том же цикле ведёт себя точно так же. lambda тут только делает ошибку короче на вид.

Частые заблуждения

Утверждение

lambda быстрее def, потому что она короче

На самом деле

Она не быстрее и не медленнее: это объект одного типа. type(named) is type(anon) даёт True, байт-код тела совпадает инструкция в инструкцию, время вызова — 22,7 нс против 22,7 нс на 3.13.7, то есть буквально одно и то же. В повторных прогонах порядок этих двух меняется местами.

Утверждение

ошибка с [lambda: i for i in range(3)] — особенность lambda

На самом деле

FAQ говорит обратное прямо: this behaviour is not peculiar to lambdas, but applies to regular functions too (это поведение свойственно не только лямбдам, но и обычным функциям). Проверено: цикл с обычным def даёт те же [2, 2, 2]. Дело в том, что функция замыкает переменную, а не значение, — и lambda тут просто самый короткий способ создать функцию в цикле.

Утверждение

значение просто не успело сохраниться

На самом деле

Сохранять было нечего: сохранена ссылка. У трёх функций РОВНО ОДНА ячейка замыкания — проверено по id, — и в ней лежит последнее значение. Обход через lambda i=i: i работает не потому, что «копирует», а потому, что аргумент по умолчанию вычисляется при создании функции: у починенного варианта __closure__ вообще None.

Утверждение

PEP 8 запрещает lambda

На самом деле

Запрещён один случай — присваивание лямбды имени: Always use a def statement instead of an assignment statement that binds a lambda expression directly to an identifier (всегда используйте инструкцию def вместо присваивания, связывающего лямбда-выражение прямо с именем). Причина названа там же и проверяема: в трассировке вместо имени будет <lambda>. Передавать лямбду аргументом PEP 8 не запрещает.

Утверждение

в lambda можно всё, просто в одну строку

На самом деле

Не проходит ничего, что инструкция, а не выражение. Проверено компиляцией: =, return, raise и assert дают SyntaxError; проходит только выражение — включая морж :=. Отдельно и важно: аннотация возврата тоже SyntaxError, поэтому типизированная функция обязана быть def.

Утверждение

key=lambda — узкое место сортировки

На самом деле

Узкое место не в lambda, а в том, что ключ достаёт код на Python. Замер: key=lambda — 7,97 мкс, key=def — 7,98 (неотличимо), key=itemgetter(1) — 5,30. Полтора раза в этом замере даёт переход на operator, а не выбор между двумя способами написать функцию; на других данных множитель будет другим, а направление тем же.

Утверждение

переменная включения ведёт себя как переменная цикла

На самом деле

Не совсем, и разница видна в отладке. После for x in range(3) переменная остаётся снаружи: x == 2, и если позже написать x = 100, все три функции начнут возвращать 100. У включения своя область видимости — переменной снаружи нет вовсе. Результат при этом одинаковый: [2, 2, 2].

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Слева знаменитая ошибка с лямбдой в цикле, справа — тот же цикл с обычным def. Что напечатает этот код?
lambdas = [lambda: i for i in range(3)]

defs = []
for i in range(3):
  def made():
      return i
  defs.append(made)

print([f() for f in lambdas])
print([f() for f in defs])

Практика · оцените

Сортировка тысячи словарей по одному ключу: key=lambda row: row['k'] против key=operator.itemgetter('k'). Во сколько раз itemgetter быстрее?
раза

Проверка знаний

Вопрос 1 из 4

Что вернёт [f() for f in [lambda: i for i in range(3)]]?

Чем измерено

Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Источники и что читать дальше

6 ИСТОЧНИКОВ

  1. Справочник языка — лямбда-выраженияОфициальная документация. Грамматика в одну строку: `lambda [varargslist] ": " expression`. Оттуда же два ограничения, которые проверяются компиляцией: «The lambda's body is restricted to a single expression» (Тело лямбды ограничено одним выражением) и «lambda forms can only contain expressions, not statements» (формы lambda могут содержать только выражения, но не инструкции).https://docs.python.org/3.14/reference/expressions.html#lambda
  2. FAQ по программированию — почему лямбды из цикла возвращают одно и то жеОфициальная документация. Отвечает ровно на тот вопрос, которому посвящён раздел про цикл, и заканчивается фразой, которую обычно не дочитывают: «Note that this behaviour is not peculiar to lambdas, but applies to regular functions too» (Заметьте, что это поведение свойственно не только лямбдам, но и обычным функциям). Проверено запуском: обычный def в цикле даёт тот же результат.https://docs.python.org/3.14/faq/programming.html
  3. PEP 8 — про присваивание lambda имениPEP. «Always use a def statement instead of an assignment statement that binds a lambda expression directly to an identifier» (Всегда используйте инструкцию def вместо присваивания, связывающего лямбда-выражение прямо с именем), и причина названа там же: «the name of the resulting function object is specifically 'f' instead of the generic '<lambda>'. This is more useful for tracebacks» (имя получившегося объекта-функции — именно „f“, а не безликое „<lambda>“. Это полезнее для трассировок). Проверено: в последнем кадре трассировки действительно стоит `<lambda>`.https://peps.python.org/pep-0008/
  4. PEP 3107 — Function AnnotationsPEP. Коллин Уинтер и Тони Лаундс, Final, Python 3.0. Отсутствие аннотаций у лямбды — не побочный эффект, а записанное решение: «Lambda's syntax does not support annotations. The syntax of lambda could be changed to support annotations, by requiring parentheses around the parameter list» (Синтаксис lambda не поддерживает аннотации. Его можно было бы изменить и добавить их, потребовав скобки вокруг списка параметров). Проверено компиляцией: `lambda x -> int: x` даёт SyntaxError на всех четырёх версиях.https://peps.python.org/pep-3107/
  5. PEP 572 — Assignment ExpressionsPEP. Крис Анджелико, Тим Питерс, Гвидо ван Россум; Final, Python 3.8. Морж — именно ВЫРАЖЕНИЕ: «The value of such a named expression is the same as the incorporated expression, with the additional side-effect that the target is assigned that value» (Значение такого именованного выражения совпадает со значением вложенного в него выражения; побочный эффект в том, что цели присваивается это же значение). Поэтому он и работает внутри лямбды, в отличие от обычного присваивания.https://peps.python.org/pep-0572/
  6. operator — itemgetter и attrgetterОфициальная документация. Модуль, к которому сводится практический вывод урока про цену `key=`. Замер: сортировка с `itemgetter(1)` в полтора раза быстрее, чем с эквивалентной `lambda`, — и разница устойчива между запусками, в отличие от разницы между `lambda` и `def`.https://docs.python.org/3.14/library/operator.html