Функциональные выражения

Перед изучением этого раздела настоятельно рекомендуется прочитать про команду привязки/перепривязки.

Теорминимум

Питон поддерживает так называемые лямбда-выражения с синтаксисом

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))