Функциональные выражения
Перед изучением этого раздела настоятельно рекомендуется прочитать про команду привязки/перепривязки.
Теорминимум
Питон поддерживает так называемые лямбда-выражения с синтаксисом
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))