Функціональне програмування на мові python

Хоча користувачі зазвичай думають про Python як про процедурному і об'єктно-орієнтованої мови, він містить все необхідне для підтримки повністю функціонального підходу до програмування.
У цій статті розглядаються загальні концепції функціонального програмування і ілюструються способи реалізації функціонального підходу на Python.

Що таке Python?


Python - вільно поширюваний, дуже високорівнева мова, що інтерпретується, розроблений Гвідо ван Россум (Guido van Rossum). Він поєднує прозорий синтаксис з потужною (але необов'язковою) об'єктно-орієнтованої семантикою. Python доступний майже на всіх існуючих нині платформах і має дуже високу переносимість між платформами.

Що таке функціональне програмування?


Найкраще почати з найважчого питання - а що, власне, таке "функціональне програмування (FP)"? Один з можливих відповідей - "це коли ви пишете на мові зразок Lisp, Scheme, Haskell, ML, OCAML, Clean, Mercury або Erlang (або ще на деяких інших)". Ця відповідь, безумовно, вірний, але не сильно прояснює суть. На жаль, отримати чітку думку про те, що ж таке FP, виявляється дуже важко навіть серед власне функціональних програмістів. Пригадується притча про трьох сліпців і слона. Можливо також визначити FP, протиставивши його "імперативного програмування" (тому, що ви робите на мовах зразок C, Pascal, C ++, Java, Perl, Awk, TCL і на багатьох інших - по крайній мірі, здебільшого).

* Функції - об'єкти першого класу. Тобто все, що можна робити з "даними", можна робити і з функціями (на кшталт передачі функції іншої функції як параметр).
* Використання рекурсії в якості основної структури контролю потоку управління. У деяких мовах не існує іншої конструкції циклу, крім рекурсії.
* Акцент на обробці списків (lists, звідси назва Lisp - LISt Processing). Списки з рекурсивним обходом подсписков часто використовуються в якості заміни циклів.
* "Чисті" функціональні мови уникають побічних ефектів. Це виключає майже повсюдно поширений в імперативних мовах підхід, при якому одній і тій же змінної послідовно присвоюються різні значення для відстеження стану програми.
* FP не схвалює або зовсім забороняє затвердження (statements), використовуючи замість цього обчислення виразів (тобто функцій з аргументами). У граничному випадку, одна програма є один вислів (плюс додаткові визначення).
* FP акцентується на тому, що має бути обчислено, а не як.
* Велика частина FP використовує функції "високого порядку" (функції, які оперують функціями, які оперують функціями).

Захисники функціонального програмування доводять, що всі ці характеристики призводять до більш швидкій розробці більш короткого і безпомилкового коду. Більш того, високі теоретики від комп'ютерної науки, логіки і математики знаходять, що процес докази формальних властивостей для функціональних мов і програм багато простіше, ніж для імперативних.

Функціональні можливості, властиві Python


Python підтримує більшу частину характеристик функціонального програмування, починаючи з версії Python 1.0. Але, як більшість можливостей Python, вони присутні в дуже змішаному мовою. Так само як і з об'єктно-орієнтованими можливостями Python, ви можете використовувати те, що вам потрібно, і ігнорувати все інше (поки воно вам не знадобиться). В Python 2.0 було додано дуже вдале "синтаксичне прикраса" - спискові вбудовування (list comprehensions). Хоча і не додаючи принципово нових можливостей, спискові вбудовування роблять використання багатьох старих можливостей значно приємніше.

Базові елементи FP в Python - функції map (), reduce (), filter () і оператор lambda. В Python 1.x введена також функція apply (), зручна для прямого застосування функції до списку, що повертається інший. Python 2.0 надає для цього покращений синтаксис. Дещо несподівано, але цих функцій і всього декількох базових операторів майже досить для написання будь-якої програми на Python; зокрема, всі керуючі затвердження ( 'if', 'elif', 'else', 'assert', 'try', 'except', 'finally', 'for', 'break', 'continue', 'while' , 'def') можна представити у функціональному стилі, використовуючи виключно функції і оператори. Незважаючи на те, що завдання реального видалення всіх команд управління потоком, можливо, корисна тільки для подання на конкурс "незрозумілий Python" (з кодом, що виглядає як програма на Lisp'е), варто усвідомити, як FP висловлює керуючі структури через виклики функцій і рекурсию.

Виняток команд управління потоком


Перше, про що варто згадати в нашому вправі - то, що Python "з'єднує безпосередньо" обчислення логічних вираженій.1 Виявляється, це надає еквівалент блоку 'if' / 'elif' / 'else' у вигляді виразу. Отже:

# ------ "короткозамкнені" умовні виклики в Python ----- #
# Звичайні керуючі конструкції
if : Func1 ()
elif : Func2 ()
else: func3 ()

# Еквівалентна "накоротко замкнутий" вираз
( and func1 ()) or ( and func2 ()) or (func3 ())

# Приклад "накоротко замкнутого" вираження
>>> x = 3
>>> def pr (s): return s
>>> (x == 1 and pr ( 'one')) or (x == 2 and pr ( 'two')) or (pr ( 'other'))
'Other'
>>> x = 2
>>> (x == 1 and pr ( 'one')) or (x == 2 and pr ( 'two')) or (pr ( 'other'))
'Two'


Здавалося б, наша версія умовних викликів за допомогою виразів - не більше, ніж салонний фокус; проте все стає набагато цікавіше, якщо врахувати, що оператор lambda може містити тільки вираження! Раз, як ми тільки що показали, вирази можуть містити умовні блоки, використовуючи коротке замикання, вираз lambda дозволяє в загальній формі представити умовні повернені значення. Базуючись на попередньому прикладі:

# --------- Lambda з короткозамкненими умовними виразами в Python ------- #
>>> pr = lambda s. s
>>> namenum = lambda x. (X == 1 and pr ( "one"))
. or (x == 2 and pr ( "two"))
. or (pr ( "other"))
>>> namenum (1)
'One'
>>> namenum (2)
'Two'
>>> namenum (3)
'Other'

Функції як об'єкти першого класу


Наведені приклади вже засвідчили, хоча і неочевидним чином, статус функцій як об'єктів першого класу в Python. Справа в тому, що, створивши об'єкт функції оператором lambda, ми зробили надзвичайно загальну дію. Ми мали можливість прив'язати наш об'єкт до імен pr і namenum в точності тим же способом, як могли б прив'язати до цих імен число 23 або рядок "spam". Але точно так само, як число 23 можна використовувати, не прив'язуючи до жодного імені (наприклад, як аргумент функції), ми можемо використовувати об'єкт функції, створений lambda, що не прив'язуючи до жодного імені. Функція в Python - всього лише ще одне значення, з яким можна щось зробити.

Головне, що ми робимо з нашими об'єктами першого класу - передаємо їх у вбудовані функції map (), reduce () і filter (). Кожна з цих функцій приймає об'єкт функції в якості першого аргументу. map () застосовує передану функцію до кожного елементу в переданому списку (списках) і повертає список результатів. reduce () застосовує передану функцію до кожного значення в списку і до внутрішнього накопичувача результату; наприклад, reduce (lambda n, m: n * m, range (1,10)) означає 10! (Факторіал 10 - помножити кожен елемент на результат попереднього множення). filter () застосовує передану функцію до кожного елементу списку і повертає список тих елементів вихідного списку, для яких передана функція повернула значення істинності. Ми також часто передаємо функціональні об'єкти нашим власним функцій, але частіше деяким комбінаціям вищезазначених вбудованих функцій.

Комбінуючи три цих вбудованих FP-функції, можна реалізувати несподівано широкий діапазон операцій потоку управління, не вдаючись до тверджень (statements), а використовуючи лише вираження.

Функціональні цикли в Python


Заміна циклів на вираження так само проста, як і заміна умовних блоків. 'For' може бути прямо переведено в map (). Так само, як і з умовним виконанням, нам знадобиться спростити блок тверджень до одного виклику функції (ми близькі до того, щоб навчитися робити це в загальному випадку):

# ---------- Функціональний цикл 'for' в Python ---------- #
for e in lst. func (e) # цикл на затвердження "for '
map (func. lst) # цикл, заснований на map ()


До речі, схожа техніка застосовується для реалізації послідовного виконання програми, використовуючи функціональний підхід. Тобто імперативне програмування здебільшого складається з тверджень, що вимагають "зробити це, потім зробити те, потім зробити щось ще". 'Map ()' дозволяє це висловити так:

# ----- Функціональне послідовне виконання в Python ---------- #
# Створимо допоміжну функцію виклику функції
do_it = lambda f. f ()

# Нехай f1, f2, f3 (etc) - функції, які виконують корисні дії
map (do_it. [f1. f2. f3]) # послідовне виконання, реалізоване на map ()


У загальному випадку, вся головна програма може бути викликом 'map ()' зі списком функцій, які треба послідовно викликати, щоб виконати програму. Ще одне зручне властивість функцій як об'єктів - то, що ви можете помістити їх в список.

Перекласти 'while' прямо трохи складніше, але цілком виходить:

# -------- Функціональний цикл 'while' в Python ---------- #
# Звичайний (заснований на затвердження "while ') цикл
while :


if :
break
else:

# Рекурсивний цикл в функціональному стилі
def while_block ():


if :
return 1
else:

return 0

while_FP = lambda. ( and while_block ()) or while_FP ()
while_FP ()


Наш варіант 'while' все ще вимагає функцію while_block (), яка сама по собі може містити не тільки вираження, а й утвердження (statements). Але ми могли б продовжити подальше виключення тверджень в цій функції (як, наприклад, заміну блоку 'if / else' для наведеного вище шаблоні на короткозамкнені вираз). До того ж, звичайна перевірка на місці (На зразок 'while myvar == 7') навряд чи буде корисною, оскільки тіло циклу (в представленому вигляді) не може змінити будь-які змінні (хоча глобальні змінні можуть бути змінені в while_block ()). Один із способів застосувати більш корисне умова - змусити while_block () повертати більш осмислене значення і порівнювати його з умовою завершення. Варто поглянути на реальний приклад виключення тверджень:

# ---------- Функціональний цикл 'echo' в Python ------------ #
# Імперативна версія "echo ()"
def echo_IMP ():
while 1:
x = raw_input ( "IMP -")
if x == 'quit':
break
else
print x
echo_IMP ()

# Службова функція, що реалізує "тотожність з побічним ефектом"
def monadic_print (x):
print x
return x

# FP версія "echo ()"
echo_FP = lambda. monadic_print (raw_input ( "FP -")) == 'quit' or echo_FP ()
echo_FP ()


Ми досягли того, що висловили невелику програму, що включає введення / виведення, цикли і умови в вигляді чистого вираження з рекурсією (фактично - у вигляді функціонального об'єкта, який при необхідності може бути переданий будь-куди). Ми все ще використовуємо службову функцію monadic_print (), але ця функція абсолютно загальна і може використовуватися в будь-яких функціональних виразах. які ми створимо пізніше (це одноразові витрати) .2 3 Зауважте, що будь-який вираз, що містить monadic_print (x) обчислюється так само, як якщо б воно містило просто x. В FP (зокрема, в Haskell) є поняття "монади" для функції, яка "не робить нічого, і викликає побічний ефект при виконанні".

Виняток побічних ефектів


Після всієї проведеної роботи по рятуванню від абсолютно осмислених конструкцій і заміні їх на незрозумілі вкладені вирази, виникає природне запитання - "Навіщо ?!". Перечитуючи мої опису характеристик FP, ми можемо бачити, що всі вони досягнуті в Python. Але найважливіша (і, швидше за все, в найбільшою мірою реально використовувана) характеристика - виняток побічних ефектів або, принаймні, обмеження їх застосування спеціальними областями на зразок монад. Величезний відсоток програмних помилок і головна проблема, що вимагає застосування отладчиков, трапляється через те, що змінні отримують невірні значення в процесі виконання програми. Функціональне програмування обходить цю проблему, просто зовсім не привласнюючи значення змінним.

Погляньмо на абсолютно звичайний ділянку імперативного коду. Його мета - роздрукувати список пар чисел, чий твір більше 25. Числа, що складають пари, самі беруться з двох інших списків. Все це дуже нагадує те, що програмісти реально роблять у багатьох ділянках своїх програм. Імперативний підхід до цього завдання міг би виглядати так:

# --- Імперативний код для "друку творів" ---- #
# Процедурне стиль - пошук великих творів за допомогою вкладених циклів
xs = (1. 2. 3. 4)
ys = (10. 15. 3. 22)
bigmuls = []
#. інший код.
for x in xs:
for y in ys:
#. інший код.
if x * y> 25:
bigmuls. append ((x. y))
#. інший код.
#. інший код.
print bigmuls

Функціональний підхід до нашого завдання повністю виключає помилки, пов'язані з побічними ефектами. Можливе рішення могло б бути таким:

# --- Функціональний код для пошуку / друку великих творів на Python ---- #
bigmuls = lambda xs. ys. filter (lambda (x. y): x * y> 25. combine (xs. ys))
combine = lambda xs. ys. map (None. xs * len (ys), dupelms (ys. len (xs)))
dupelms = lambda lst. n. reduce (lambda s. t. s + t. map (lambda l. n = n. [l] * n. lst))
print bigmuls ((1. 2. 3. 4), (10. 15. 3. 22))


Ми пов'язуємо в прикладі анонімні ( 'lambda') функції з іменами, але це не потрібно. Замість цього ми могли просто вкласти визначення. Ми використовували імена як заради більшої читабельності, так і тому, що combine () - в будь-якому випадку відмінна службова функція (генерує список всіх можливих пар елементів з двох списків). У свою чергу, dupelms () в основному лише допоміжна частина combine (). Хоча цей функціональний приклад більш багатослівний, ніж імперативний, при повторному використанні службових функцій код в власне bigmuls () виявиться, ймовірно, більш лаконічним, ніж в імперативний варіанті.

Реальна перевага цього функціонального прикладу в тому, що в ньому абсолютно жодна змінна не змінює свого значення. Будь-яке несподіване побічний вплив на подальший код (або з боку попереднього коду) просто неможливо. Звичайно, саме по собі відсутність побічних ефектів не гарантує безпомилковість коду, але в будь-якому випадку це перевага. Однак зауважте, що Python, на відміну від багатьох функціональних мов, не запобігає повторне прив'язування імен bigmuls, combine і dupelms. Якщо далі в процесі виконання програми combine () почне означати щось інше - на жаль! Можна було б розробити клас-одиночку (Singleton) для підтримки одноразового зв'язування такого типу (напр. 'S.bigmuls', etc.), але це виходить за рамки цієї статті.

Ще варто відзначити, що завдання, яке ми тільки що вирішили, скроєна в точності під нові можливості Python 2.0. Замість вищенаведених прикладів - імперативного або функціонального - найкраща (і функціональна) техніка виглядає наступним чином:

# ----- Код Python для "bigmuls" з використанням облікових вбудовування (list comprehensions) ----- #
print [(x. y) for x in (1. 2. 3. 4) for y in (10. 15. 3. 22) if x * y> 25]

висновок

Схожі статті