Функциональные выражения
Перед изучением этого раздела настоятельно рекомендуется прочитать про команду привязки/перепривязки.
Теорминимум
Питон поддерживает так называемые лямбда-выражения с синтаксисом
lambda переменные_через_запятую: формула
Лямбда-выражения имеют нетривиальную семантику взаимодействия
с оператором применения. А именно, если значение формулы FOO
является значением лямбда-выражения lambda BAR: BAZ
, то
формула
FOO(QUUZ)
вычисляется следующим образом:
- вычисляется
FOO
- вычисляются все формулы последовательности
QUUZ
- для каждой из переменных
BAR
создаётся временная привязка к значению соответствующей формулыQUUZ
- в контексте этих привязок вычисляется формула
BAZ
- значение которой становится значением всей формулы
FOO(QUUZ)
Рассмотрим, например, программу
x = 1
print((lambda x: x+2)(x+1))
print(x)
Она выполняется следующим образом:
- создаётся привязка
x
к числу1
- вычисляется значение переменной
print
: это — встроенная подпрограмма вывода - вычисляется значение формулы
lambda x: x+2
: можно считать, что её значение — она сама - вычисляется значение переменной
x
: это — её текущая привязка, то есть число1
- вычисляется значение литерала
1
: это — число1
- вычисляется значение оператора сложения, на вход которому поданы две единицы:
это — число
2
- создаётся временная привязка
x
к числу2
- в контексте этой привязки вычисляется
x
: это — число2
- вычисляется значение литерала
2
: это — число2
- вычисляется значение оператора сложения, на вход которому поданы две двойки:
это — число
4
- временная привязка
x
к числу2
удаляется - оператору применения подаются на вход встроенная подпрограмма вывода и
число
4
: его результат — специальное значениеNone
- как побочный эффект вычисления значения оператора применения
программа отправляет
4
на стандартный вывод - вычисляется значение переменной
print
: это — встроенная подпрограмма вывода - вычисляется значение переменной
x
: сейчас это — число1
- оператору применения подаются на вход встроенная подпрограмма вывода и
число
1
: его результат — специальное значениеNone
- как побочный эффект вычисления значения оператора применения
программа отправляет
1
на стандартный вывод
Итого мы увидим на стандартном выводе программы числа 4 и 1.
Лексические замыкания
Ключевая и весьма неочевидная для людей, не знакомых с классическим лямбда-исчислением, особенность лямбда-выражений — способность к созданию лексических замыканий. Это означает буквально следующее:
- контекст (набор привязок), в котором вычисляется подформула лямбда-выражения — это тот контекст, в котором было вычислено само лямбда-выражение
Необходимый пример-пояснение:
x = 2
foo = (lambda x: (lambda: print(x)))(1)
print(x) ## печатает 2
foo() ## печатает 1
В этой программе foo()
отправляет на стандартный вывод число 1,
так как лямбда-выражение lambda: print(x)
было вычислено в
контексте временной привязки x --> 1
, которая в тот момент
закрывала глобальную привязку x --> 2
.
К сожалению, лексические замыкания имеют неоднозначные взаимоотношения с перепривязкой переменных. А именно, на первый взгляд программа
x = 1
foo = lambda: print(x)
x = 2
foo()
должна была бы печатать 1
, так как лямбда-выражение было вычислено
в контексте привязки x --> 1
.
Но программа печатает 2
банально по той причине, что команда перепривязки
создаёт не новую привязку, а изменяет имеющуюся.
При этом есть языки, в которых программа, аналогичная вышеприведённой, ведёт себя не так, как в Питоне. Например, следующая программа на языке Elixir
x = 1
foo = fn -> IO.puts(x) end
x = 2
foo.()
печатает именно 1, а не 2, поскольку в Elixir команда привязки всегда создаёт новую привязку.
Замыкания и рекурсия
Напоследок отметим, что команда привязки в Питоне имеет следующий порядок работы:
- если переменной в контексте нет, она там создаётся
- затем вычисляется формула в правой части
- затем привязка переменной меняется на значение формулы
Это позволяет успешно работать следующей программе:
fac = lambda n: (1 if n < 2 else n * fac(n-1))
print(fac(5))