Тонкощі використання мови Python Частина 3
Функції у Python
Функції в Python визначаються двома способами: через визначенняdef або через анонімний описlambda. Обидва ці способи визначення доступні, в тій чи іншій мірі, і деяких інших мовах програмування. Особливістю Python є те, що функція є таким самим іменованим об'єктом, як і будь-який інший об'єкт деякого типу даних, скажімо, як ціла змінна. У лістингу 1 представлений найпростіший приклад (файлfunc.py з архівуpython_functional.tgz у розділі "Матеріали для скачування"):
Лістинг 1. Визначення функцій
При виклику всіх трьох об'єктів-функцій ми отримаємо той самий результат:
Ще більш виразно це проявляється в Python версії 3, в якій все є класами (у тому числі і цілочисленна змінна), а функції є об'єктами програми, що належать до класуfunction :
Примітка. Існують ще 2 типи об'єктів, що допускають функціональний виклик - функціональний метод класу та функтор, про які ми поговоримо пізніше.
Якщо функціональні об'єкти Python є такими ж об'єктами, як і інші об'єкти даних, значить, з ними можна робити все те, що можна робити з будь-якими даними:
- динамічнозмінювати у ході виконання;
- вбудовувати у складніші структури даних (колекції);
- передавати як параметри і значення, що повертаються і т.д.
На цьому (маніпуляції з функціональними об'єктами як з об'єктами даних) і базується функціональне програмування. Python, звичайно, не є справжньою мовою функціонального програмування, так, для повністю функціональногоПрограмування існують спеціальні мови: Lisp, Planner, а з свіжіших: Scala, Haskell. Ocaml, . Але в Python можна "вбудовувати" прийоми функціонального програмування в загальний потік імперативного (командного) коду, наприклад, використовувати методи, запозичені з повноцінних функціональних мов. Тобто. "згортати" окремі фрагменти імперативного коду (іноді досить великого обсягу) у функціональні вирази.
Часом запитують: «Чому переваги функціонального стилю написання окремих фрагментів для програміста?». Основною перевагою функціонального програмування є те, що після одноразового налагодження такого фрагмента в ньому при подальшому багаторазовому використанні не виникнуть помилки за рахунок побічних ефектів, пов'язаних із присвоєннями та конфліктом імен.
Досить часто при програмуванні на Python використовують типові конструкції з функціонального програмування, наприклад:
В результаті запуску отримуємо:
Функції як об'єкти
Створюючи об'єкт функції операторомlambda, як було показано в лістингу 1, можна прив'язати створений функціональний об'єкт до іменіpow3 так само, як можна було б прив'язати до цього імені число123 або рядок"Hello!". Цей приклад підтверджує статус функцій як об'єктів першого класу Python. Функція в Python — це лише одне значення, з яким можна щось зробити.
Найчастіша дія, що виконується з функціональними об'єктами першого класу, - це передача їх у вбудовані функції вищого порядку:map(),reduce() таfilter(). Кожна з цих функцій приймає об'єкт функції як перший аргумент.
- map() застосовує передану функцію до кожногоелементу в переданому списку (списках) та повертає список результатів (тої ж розмірності, що й вхідний);
- reduce() застосовує передану функцію до кожного значення у списку та до внутрішнього накопичувача результату, наприклад,reduce( lambda n,m: n * m, range( 1, 10 ) ) означає10! (факторіал);
- filter() застосовує передану функцію кожного елемента списку і повертає список тих елементів вихідного списку, котрим передана функція повернула значення істинності.
Комбінуючи ці три функції, можна реалізувати несподівано широкий діапазон операцій потоку управління, не вдаючись до імперативних тверджень, а використовуючи лише вирази у функціональному стилі, як показано у лістингу 2 (файлfuncH.py з архівуpython_functional .tgz у розділі "Матеріали для скачування"):
Лістинг 2. Функції вищих систем Python
Примітка. Цей код дещо ускладнений у порівнянні з попереднім прикладом через наступні аспекти, пов'язані з сумісністю Python версій 2 і 3:
- Функціяreduce(), оголошена як вбудована в Python 2, в Python 3 була винесена в модульfunctools і її прямий виклик на ім'я викликає винятокNameError, тому для коректної роботи виклик повинен бути оформлений як у прикладі або включати рядок:from functools import *
- Функціїmap() іfilter() у Python 3 повертають не список (що вже показувалося при обговоренні відмінностей версій), а об'єкти-ітератори виду:
Для отримання всього списку значень їм викликається функціяlist().
Тому такий код зможе працювати в обох версіях Python:
Якщо переносність коду між різними версіями не потрібна, то подібні фрагментиможна виключити, що дозволить дещо спростити код.
У функціональному програмуванні рекурсія є основним механізмом, аналогічно циклам в ітеративному програмуванні.
У деяких обговореннях щодо Python неодноразово доводилося зустрічатися із заявами, що в Python глибина рекурсії обмежена "апаратно", і тому деякі дії реалізувати неможливо в принципі. В інтерпретаторі Python дійсно встановлено обмеження глибини рекурсії, рівним 1000, але це чисельний параметр, який завжди можна перевстановити, як показано в лістингу 3 (повний код прикладу можна знайти у файліfact2.py з архівуpython_functional.tgz у розділі "Матеріали для скачування"):
Лістинг 3. Обчислення факторіалу з довільною глибиною рекурсії
Ось як виглядає виконання цього прикладу в Python 3 і Python2 (правда насправді отримане число навряд чи поміститься на один екран терміналу консолі):
Декілька найпростіших прикладів
Виконаємо декілька найпростіших трансформацій звичного імперативного коду (командного, операторного) для перетворення його окремих фрагментів на функціональні. Спочатку замінимо оператори розгалуження логічними умовами, які рахунок " відкладених " (lazy, лінивих) обчислень дозволяють управляти виконанням чи невиконанням окремих гілок коду. Так, імперативна конструкція:
- повністю еквівалентна наступному функціональному фрагменту (за рахунок "відкладених" можливостей логічних операторівand таor ):
Як приклад знову використовуємо обчислення факторіалу. У лістингу 4 наведено функціональний код для обчислення факторіалу (файлfact1.py в архівіpython_functional.tgz у розділі "Матеріали для скачування"):
Лістинг 4.Операторне (імперативне) визначення факторіалу
Аргумент для обчислення витягується із значення параметра командного рядка (якщо він є) або вводиться з терміналу. Перший варіант зміни, показаний вище, вже застосовується в лістингу 2 де на функціональні вирази були замінені:
- визначення функції факторіалу:
- запит на введення значення аргументу з консолі терміналу:
У файліfact3.py з'являється ще одне визначення функції, зроблене через функцію вищого порядкуreduсe() :
Тут ми спростимо також і вираз дляn, звівши його до одноразового виклику анонімної (не іменованої) функції:
Нарешті, можна побачити, що присвоєння значення змінноїn потрібно лише її використання у викликуprint() виведення цього значення. Якщо ми відмовимося і від цього обмеження, то вся програма виродиться в один функціональний оператор (див. файлfact4.py в архівіpython_functional.tgz у розділі "Матеріали для скачування"):
Цей єдиний виклик всередині функціїprint() і представляє весь додаток у його функціональному варіанті:
Чи читається цей код (файл fact4.py) краще за імперативний запис (файл fact1.py)? Скоріше ні ніж так. У чому тоді його гідність? У тому, що прибудь-яких змінах навколишнього коду, нормальна роботацього фрагмента збережеться, тому що відсутня ризик побічних ефектів через зміну значень змінних, що використовуються.
Функції вищих порядків
При функціональному стилі програмування стандартною практикою є динамічна генерація функціонального об'єкта в процесі виконання коду, з його подальшим викликом у тому ж коді. Існує ціла низка областей, де подібнатехніка може виявитися корисною.
Одне з цікавих понять функціонального програмування - це замикання (closure). Ця ідея виявилася настільки привабливою для багатьох розробників, що була реалізована навіть у деяких нефункціональних мовах програмування (Perl). Девід Мертц наводить таке визначення замикання: "Замикання - це процедура разом із прив'язаною до неї сукупністю даних" (на противагу об'єктам в об'єктному програмуванні, як: "дані разом із прив'язаним до них сукупністю процедур").
Сенс замикання полягає в тому, що визначення функції "заморожує" навколишній контекст на момент визначення . Це може робитись різними способами, наприклад, за рахунок параметризації створення функції, як показано в лістингу 5 (файлclos1.py в архівіpython_functional.tgz у розділі "Матеріали для скачування"):
Лістинг 5. Створення замикання
Ось як спрацьовує така динамічно певна функція:
Інший спосіб створення замикання - це використання значення параметра за замовчуванням у точці визначення функції, як показано в лістингу 6 (файлclos3.py з архівуpython_functional.tgz у розділі "Матеріали для скачування" ):
Лістинг 6. Інший спосіб створення замикання
Ніякі наступні надання значень параметру за умовчанням не призведуть до зміни раніше визначеної функції, але сама функція може бути перевизначена:
Часткове застосування функції
Часткове застосування функції передбачає на основі функціїN змінних визначення нової функції з меншим числом зміннихM N , при цьому іншіN — M змінних набувають фіксованих "заморожених" значень. (використовується модульfunctools ). Подібний приклад будерозглянуто нижче.
Функтор — це функція, а об'єкт класу, у якому визначено метод з ім'ям__call__(). При цьому для екземпляра такого об'єкта може застосовуватися виклик, так само, як це відбувається для функцій. У лістингу 7 (файлpart.py з архівуpython_functional.tgz у розділі "Матеріали для скачування") демонструється використання замикання, часткового визначення функції та функтора, що призводять до отримання одного і того ж результату.
Лістинг 7. Порівняння замикання, часткового визначення та функтора
Виклик усіх трьох конструкцій для аргументу, що дорівнює 5, призведе до отримання однакового результату, хоча при цьому і використовуватимуться абсолютно різні механізми:
Каррінг (або карірування, curring) - перетворення функції від багатьох змінних на функцію, що бере свої аргументи по одному.
Примітка. Це перетворення було введено М. Шейнфінкелем і Г. Фреге і отримало свою назву на честь математика Хаскелла Каррі, на честь якого названо також мову програмування Haskell.
Карринг не відноситься до унікальних особливостей функціонального програмування, тому карингове перетворення може бути записано, наприклад, і мовами Perl або C++. Оператор карірування навіть вбудований у деякі мови програмування (ML, Haskell), що дозволяє багатомісні функції призводити до карованого представлення. Але всі мови, що підтримують замикання, дозволяють записувати функцію карування, і Python не є винятком у цьому плані.
У лістингу 8 представлений найпростіший приклад з використанням каринга (файлcurry1.py в архівіpython_functional.tgz у розділі "Матеріали для скачування"):
Лістинг 8. Каринг
Ось як виглядають виконання цих викликів:
Висновок
У наступній статті ми обговоримо питання організації паралельного виконання коду серед Python.