Слідкуйте за виходом нових статей цієї серії.
Цей контент є частиною серії: Чарівний Python
Слідкуйте за виходом нових статей цієї серії.
У попередній статті "Функціональне програмування на Python" були висвітлені основні поняття функціонального програмування (ФП). У цій статті ми спробуємо трохи заглибитися в цю багатющу концептуальну область. Бібліотека Xoltar Toolkit Бріна Келлера (Bryn Keller) надасть нам в цьому неоціненну допомогу. Основні можливості ФП Келлер представив у вигляді невеликого ефективного модуля на чистому Python. Крім модуля functional. в Xoltar Toolkit входить модуль lazy. підтримує структури, що обчислюються "тільки коли це необхідно". Безліч функціональних мов програмування підтримують відкладене обчислення, тому ці компоненти Xoltar Toolkit нададуть вам багато чого з того, що ви можете знайти в функціональній мові зразок Haskell.
присвоєння значень
Уважний читач пам'ятає про згадуваному мною обмеження функціональної техніки, описаної в попередній статті. Йшлося про те, що ніщо в Python не забороняє перепрісваіванія іншого значення імені, що посилається на функціональне вираз. У ФП під іменами розуміється всього лише буквене скорочення довших виразів, при цьому мається на увазі, що один і той же вираз завжди призводить до одного й того ж результату. Якщо ж уже певного імені присвоюється нове значення, це припущення порушується. Припустимо, ми визначаємо неколько скорочень для використання в нашій функціональної програмою, як в наступному прикладі:
Лістинг 1. FP-сесія Python з перепрісваіванія призводить до неприємностей
>>> car = lambda lst: lst [0] >>> cdr = lambda lst: lst [1:] >>> sum2 = lambda lst: car (lst) + car (cdr (lst)) >>> sum2 ( range (10)) 1 >>> car = lambda lst: lst [2] >>> sum2 (range (10)) 5
На жаль, один і той же вираз sum2 (range (10)) обчислюється до різних результатів в двох місцях програми, незважаючи на те, що аргументи вираженні не є змінними змінними.
Лістинг 2. FP-сесія Python з захистом від перепрісваіванія
>>> from functional import * >>> let = Bindings () >>> let.car = lambda lst: lst [0] >>> let.car = lambda lst: lst [2] Traceback (innermost last): File "
Зрозуміло, реальна програма повинна перехопити і обробити виняток BindingError, проте сам факт її порушення дозволяє уникнути цілого класу проблем.
Крім класу Bindings. functional містить функцію namespace. предоставлюющую доступ до простору імен (насправді, до словника) з екземпляра класу Bindings. Це дуже зручно, якщо вам потрібно обчислити вираз в (неизменяемом) просторі імен, визначеному в Bindings. Функція eval () в Python дозволяє проводити обчислення в просторі імен. Наступний приклад пояснює сказане:
Лістинг 3. FP-сесія Python, що використовує незмінні простору імен
>>> let = Bindings () # "Real world" function names >>> let.r10 = range (10) >>> let.car = lambda lst: lst [0] >>> let.cdr = lambda lst: lst [1:] >>> eval ( 'car (r10) + car (cdr (r10))', namespace (let)) >>> inv = Bindings () # "Inverted list" function names >>> inv. r10 = let.r10 >>> inv.car = lambda lst: lst [-1] >>> inv.cdr = lambda lst: lst [: - 1] >>> eval ( 'car (r10) + car (cdr (r10)) ', namespace (inv)) 17
У ФП існує дуже цікаве поняття - замикання (closure). Насправді, ця ідея виявилася настільки привабливою для багатьох розробників, що реалізована навіть в нефункціональних мовах програмування, таких як Perl і Ruby. Крім того, схоже, що в Python 2.1 неминуче буде включений лексичний контекст [1], що на 99% наблизить нас до замикань.
Так що ж таке замикання? Стів Маджевскі (Steve Majewski) чудово охарактеризував це поняття в одній з присвячених Python мережевих конференцій: Об'єкт - це сукупність даних разом з прив'язаними до них процедурами. Замикання - це процедура разом з прив'язаною до неї сукупністю даних.
Іншими словами, замикання є щось на зразок функціонального доктора Джекілл по відношенню до об'єктно-орієнтованого містеру Хайду (або, можливо, навпаки). Замикання, так само як і екземпляр об'єкта, є спосіб представлення функціональності і даних, пов'язаних і упакованих разом.
Давайте повернемося трохи назад, щоб зрозуміти, яку проблему вирішують як об'єкти, так і замикання, і як ця проблема вирішується без них. Зазвичай результат, що повертається функцією, визначається контекстом під час її обчислення. Найзагальніший - і, можливо, самий очевидний - спосіб визначити цей контекст - це передати у функцію параметри, що вказують, які значення повинні оброблятися. Але іноді є цілком очевидна відмінність між "фоновими" і "пріоритетними" параметрами - тим часом, що робить функція в даний момент, і тим, як вона налаштована для виконання багаторазових потенційних викликів.
Існує ряд способів підтримки фонових аргументах, акцентуючи увагу на пріоритетних. Наприклад, можна просто передавати кожен необхідний аргумент у функцію при кожному її виклику. Часто це зводиться до передачі ряду значень (або структури з безліччю членів) протягом всієї послідовності викликів, щоб передати дані туди, де вони можуть знадобитися. Це ілюструється простим прикладом:
Лістинг 4. Python session showing cargo variable
У цьому прикладі, параметр n в межах функції b () потрібен тільки для того, щоб бути доступним для передачі в c () Інше можливе рішення - використання глобальних змінних.
Лістинг 5. Сесія Python, що показує використання глобальної змінної
>>> N = 10 >>> defaddN (i). global N. return i + N. >>> addN (7) # Add global N to argument 17 >>> N = 20 >>> addN (6) # Add global N to argument 26
Глобальна змінна N доступна в будь-який момент, де б ви не викликали addN (). при цьому зовсім не обов'язково явно передавати фоновий контекст. Кілька більш "пітоновская" техніка - "заморозити" змінну у функції, використовуючи для цього значення параметра за замовчуванням під час визначення функції:
Лістинг 6. Сесія Python, що ілюструє заморожену змінну
>>> N = 10 >>> defaddN (i, n = N). return i + n. >>> addN (5) # Add 10 15 >>> N = 20 >>> addN (6) # Add 10 (current N does not matter) 16
Заморожена нами змінна, по суті, замикання. Деякі дані прикріплені до функції addN (). У разі повного замикання, всі дані, які були присутні в момент опису цієї функції, були б доступні при її виклику. Однак в даному прикладі (і в багатьох серйозніших) можна просто забезпечити доступ до достатньої кількості даних за допомогою стандартних параметрів. Адже змінні, які не використовуються функцією addN (). не грають ніякої ролі при її обчисленні.
Давайте розглянемо об'єктний підхід на прикладі кілька більш нагальної проблеми. У цю пору року думки зазвичай зайняті інтерактивними програмами, використовуваними для обчислення податку. Вони збирають різні дані - необов'язково в певному порядку - а потім в якісь момент використовують їх при обчисленні. Давайте створимо спрощений варіант такої програми:
Лістинг 7. Клас в стилі Python для обчислення податку
class TaxCalc: deftaxdue (self): return (self.income-self.deduct) * self.rate taxclass = TaxCalc () taxclass.income = 50000 taxclass.rate = 0.30 taxclass.deduct = 10000 print "Pythonic OOP taxes due =" , taxclass.taxdue ()
У нашому класі TaxCalc (точніше, в його примірнику) ми можемо зібрати дані - в будь-якому порядку - і як тільки у нас будуть всі необхідні елементи, можна буде викликати метод об'єкта, щоб виконати обчислення над зібраними даними. Все зібрано в межах примірника і, тим самим, різні екземпляри можуть задавати різні набори даних. Створення безлічі екземплярів, що відрізняються один від одного тільки своїми даними, неможливо при використанні глобальних або "заморожених" змінних. Підхід "грубої сили" може вирішити цю задачу, але можна бачити, що в реальному прикладі може знадобитися передача безлічі значень. Тепер давайте подивимося, як можна вирішити цю задачу за допомогою ООП з використанням передачі повідомлень (це схоже на Smalltalk або Self, так само як і на деякі об'єктно-орієнтовані варіанти xBase, якими я користувався):
Лістинг 8. Smalltalk-style (Python) tax calculation
class TaxCalc: deftaxdue (self): return (self.income-self.deduct) * self.rate defsetIncome (self, income): self.income = income return self defsetDeduct (self, deduct): self.deduct = deduct return self defsetRate (self, rate): self.rate = rate return self print "Smalltalk-style taxes due =", \ TaxCalc (). setIncome (50000) .setRate (0.30) .setDeduct (10000) .taxdue ()
Повернення self кожним монтажником дозволяє нам розглядати поточний екземпляр як результат виклику кожного методу. Як видно надалі, цей підхід має цікаві спільні риси з використанням замикання в ФП.
Працюючи з Xoltar toolkit, можна створювати повні замикання, що мають необхідну властивість об'єднання дані з функцією, а також множинні замикання, що несуть в собі різні набори даних:
Лістинг 9. Python Functional-Style tax calculations
from functional import * taxdue = lambda. (Income-deduct) * rate incomeClosure = lambda income, taxdue: closure (taxdue) deductClosure = lambda deduct, taxdue: closure (taxdue) rateClosure = lambda rate, taxdue: closure (taxdue) taxFP = taxdue taxFP = incomeClosure (50000, taxFP ) taxFP = rateClosure (0.30, taxFP) taxFP = deductClosure (10000, taxFP) print "Functional taxes due =", taxFP () print "Lisp-style taxes due =", \ incomeClosure (50000, rateClosure (0.30, deductClosure (10000 , taxdue))) ()
Кожна описана нами функція замикання бере будь-які значення, визначені в її контексті, і прив'язує ці величини до глобальному контексті функціонального об'єкта. Однак те, що здається глобальної областю даної функції, необов'язково збігається як з "справжньою" глобальної областю дії модуля, так і з глобальної областю іншого замикання. Замикання просто "несе дані з собою".
У нашому прикладі, щоб помістити певні значення в область дії замикання, ми використовуємо кілька приватних функцій (income, deduct, rate). Було б досить просто змінити дизайн так, щоб було можна привласнювати довільні значення. Крім того, заради розваги, ми використовуємо в цьому прикладі два злегка різних функціональних стилю. Перший послідовно прив'язує додаткові значення до області замикання; зробивши taxFP змінною, ми дозволяємо рядках додати в замикання з'являтися в будь-якому порядку. Однак, якби ми використовували незмінні імена на кшталт tax_with_Income. нам довелося б розташувати зв'язування в певному порядку і передавати більш ранні наступним. У будь-якому випадку, як тільки все необхідне прив'язане до замикання, ми можемо викликати вирощену функцію.
Другий стиль, на мій погляд, дещо схожий на Lisp (виключно через присутність круглих дужок). Крім естетики, у другому стилі присутні два цікавих моменти. По-перше, повністю відсутня прив'язування імен. Другий стиль - одиночне вираз без використання директив. (Див. Попередню статті, де пояснюється, чому це грає роль).
Інша цікава деталь Lisp-стилю в тому, наскільки сильно його використання замикань нагадує методи передачі повідомлень a la Smalltalk, про які говорилося вище. В обох випадках значення накопліваются до виклику функції / методу taxdue () (обидва в цих спрощених версіях збуджують виключення, якщо необхідні дані недоступні). Smalltalk-стиль на кожному кроці передає об'єкт, в той час як Lisp-стиль - продовження (continuation). Але якщо дивитися в корінь, то функціональне і об'єктно-орієнтоване програмування призводять майже до одного й того ж.