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 принимают функцию,
потому что им нужен не готовый алгоритм, а правило: по какому значению
сравнивать, что считать подходящим.
Обычно такую функцию сначала объявляют, а потом передают:
def second(pair):
return pair[1]
rows.sort(key=second)lambda — способ записать ровно ту же функцию выражением и поставить её
прямо туда, где она нужна:
rows.sort(key=lambda pair: pair[1])Обе записи дают функцию, обе делают одно и то же. Разница в том, что во второй у функции нет отдельного имени и отдельного места в файле.
Отсюда и ответ на вопрос «когда стоит писать так». Когда функция маленькая, нужна ровно в одном месте — в самом вызове, — и отдельное имя для неё скорее мешает: заставляет читателя отвести глаза, найти определение и вернуться обратно, чтобы узнать то, что помещалось в полстроки.
И обратное правило из того же соображения: как только имя нужно — функция
вызывается из нескольких мест, ей нужна аннотация типа, её имя должно быть
видно в трассировке при падении, — пишут def. Это же говорит и PEP 8,
возражая против записи f = lambda ...: в ней имя вроде бы есть, а в
трассировке его нет.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования:
lambda — та же функция, записанная выражением, и уместна она там, где
отдельное имя ухудшает читаемость. Всё дальнейшее — про то, чем именно
lambda отличается от def (отличий немного, и скорости среди них нет), про
знаменитую ошибку с циклом, которая к lambda не относится, и про то, что на
самом деле показывают замеры.
Механизм 1: один тип, один байт-код, одно время
lambda даёт объект того же типа function, что и def. Различия — в грамматике и метаданных.Начнём с того, что проверяется тремя строками.
def named(x):
return x + 1
anon = lambda x: x + 1type(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>, и это видно там, где важнее всего. Две
функции, которые падают одинаково:
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 identifierdef вместо присваивания, связывающего лямбда-выражение прямо с именем) — потому что 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 = 1 | SyntaxError |
lambda x: return x | SyntaxError |
lambda x: raise ValueError | SyntaxError |
lambda x: assert x | SyntaxError |
lambda x -> int: x | SyntaxError — аннотация возврата не разрешена |
Последняя строка — про другое: аннотацию запрещает не правило «только
выражения», а грамматика самой лямбды, и PEP 3107 говорит об этом прямо.
На практике она важнее остальных: лямбду нельзя аннотировать. Если у
функции должен быть тип — это def, и не из вкусовых соображений.
Механизм 4: ошибка, которая не падает
А теперь код, который не падает, ничего не предупреждает — и возвращает не то.
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]:
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, и ни одного исключения при этом нет.
Обход — аргумент по умолчанию, потому что он вычисляется в момент создания функции:
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 нет |
То есть в цикле функции держат ссылку на переменную, которая живёт дальше и которую можно испортить позже. У включения своя область видимости, наружу ничего не выходит — но ячейка у трёх функций всё равно одна, и результат одинаковый.
Глубже: сколько это стоит на самом деле
Три способа достать один и тот же ключ; в таблице — время одной сортировки списка из 200 кортежей, лучшее из шестидесяти чередующихся кругов по 2 000 сортировок.
| способ | 3.13.7 | 3.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.0 | PEP 3107 даёт аннотации def и не даёт их lambda — и это записанное решение, а не недосмотр: Lambda's syntax does not support annotations (синтаксис lambda не поддерживает аннотации). С тех пор функция, которой нужен тип, обязана быть def, и это проверяется компиляцией, а не соглашением. | |
| 3.8 | PEP 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 identifierdef вместо присваивания, связывающего лямбда-выражение прямо с именем). Причина названа там же и проверяема: в трассировке вместо имени будет <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].
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
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])Практика · оцените
Проверка знаний
Что вернёт [f() for f in [lambda: i for i in range(3)]]?
Чем измерено
Числа этой статьи получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
lambda— это обычная функция, записанная выражением. Она нужна там, где функция маленькая и требуется прямо в месте вызова, — например аргументом вsorted, — и заводить для неё отдельное имя значит мешать читателю, а не помогать ему. Объект при этом получается тот же, что уdef: один тип, одинаковый байт-код тела.- Отсюда главное следствие: выбор между
lambdaиdef— про читаемость, а не про поведение. Различия лежат в грамматике и метаданных: тело лямбды обязано быть выражением, а имя у неё синтетическое —вместо настоящего, и видно это в трассировке. Знаменитая ошибка с циклом —[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.
На самом деле
- Она не быстрее и не медленнее: это объект одного типа.
type(named) is type(anon)даётTrue, байт-код тела совпадает инструкция в инструкцию, время вызова — 22,7 нс против 22,7 нс на 3.13.7, то есть буквально одно и то же. В повторных прогонах порядок этих двух меняется местами. - 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. - Запрещён один случай — присваивание лямбды имени: Always use a
defstatement instead of an assignment statement that binds a lambda expression directly to an identifier. Причина названа там же и проверяема: в трассировке вместо имени будет<lambda>. Передавать лямбду аргументом PEP 8 не запрещает. - Не проходит ничего, что инструкция, а не выражение. Проверено компиляцией:
=,return,raiseиassertдаютSyntaxError; проходит только выражение — включая морж:=. Отдельно и важно: аннотация возврата тожеSyntaxError, поэтому типизированная функция обязана бытьdef. - Узкое место не в
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].
По версиям
- 3.0
- PEP 3107 даёт аннотации
defи не даёт ихlambda— и это записанное решение, а не недосмотр: Lambda's syntax does not support annotations. С тех пор функция, которой нужен тип, обязана бытьdef, и это проверяется компиляцией, а не соглашением.< - 3.8
- PEP 572:
:=— именно выражение, а не инструкция, поэтому внутриlambdaон работает. Это единственный способ что-то «присвоить» в теле лямбды, и записьlambda x: (y := x)компилируется, в отличие отlambda x: y = x.< - 3.12
- Включения встроены в вызывающий код (PEP 709), но собственная область видимости у них осталась: переменная включения по-прежнему не видна снаружи — проверено на 3.11, 3.12, 3.13 и 3.14, результат одинаковый.<
Что разобрано
- База: обычная функция, записанная выражением
- Механизм 1: один тип, один байт-код, одно время
- Механизм 2: отличие первое — имя
- Механизм 3: отличие второе — тело обязано быть выражением
- Механизм 4: ошибка, которая не падает
- Глубже: сколько это стоит на самом деле
- Глубже: история версий
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
- Чем измерено
Источники и что читать дальше
6 ИСТОЧНИКОВ
- Справочник языка — лямбда-выраженияОфициальная документация. Грамматика в одну строку: `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
- 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
- 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/
- 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/
- 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/
- operator — itemgetter и attrgetterОфициальная документация. Модуль, к которому сводится практический вывод урока про цену `key=`. Замер: сортировка с `itemgetter(1)` в полтора раза быстрее, чем с эквивалентной `lambda`, — и разница устойчива между запусками, в отличие от разницы между `lambda` и `def`.https://docs.python.org/3.14/library/operator.html