Тонкости использования языка Python: Часть 3. Функциональное программирование

Статья является частью миницикла, посвященного тонким вопросам применения Python и разработки приложений на этом языке. В статье описаны возможности языка Python, позволяющие применять его для написания программ в функциональном стиле.

Олег Цилюрик, преподаватель тренингового отделения, Global Logic

Фото автораОлег Иванович Цилюрик, много лет был разработчиком программного обеспечения в крупных центрах разработки: ВНИИ РТ, НПО "Дельта", КБ ПМ. Последние годы работал над проектами в области промышленной автоматики, IP телефонии и коммуникаций. Автор нескольких книг. Преподаватель тренингового отделения международной софтверной компании Global Logic.



22.11.2013

Введение

Существует большое количество публикаций, посвящённых реализациям концепций функционального программирования на языке Python, но большая часть этих материалов написана одним автором - Девидом Мертцом (David Mertz). Кроме того, многие из этих статей уже устарели и разнесены по различным сетевым ресурсам. В этой статье мы попробуем снова обратиться к этой теме, чтобы освежить и упорядочить доступную информацию, особенно учитывая большие различия, имеющиеся между версиями Python линии 2 и линии 3.


Функции в Python

Функции в Python определяются 2-мя способами: через определение def или через анонимное описание lambda. Оба этих способа определения доступны, в той или иной степени, и в некоторых других языках программирования. Особенностью Python является то, что функция является таким же именованным объектом, как и любой другой объект некоторого типа данных, скажем, как целочисленная переменная. В листинге 1 представлен простейший пример (файл func.py из архива python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 1. Определения функций
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys

def show( fun, arg ):
    print( '{} : {}'.format( type( fun ), fun ) )
    print( 'arg={} => fun( arg )={}'.format( arg, fun( arg ) ) )

if len( sys.argv ) > 1: n = float( sys.argv[ 1 ] )
else: n = float( input( "число?: " ) )

def pow3( n ):                     # 1-е определение функции
    return n * n * n
show( pow3, n )

pow3 = lambda n: n * n * n         # 2-е определение функции с тем же именем
show( pow3, n )

show( ( lambda n: n * n * n ), n ) # 3-е, использование анонимного описание функции

При вызове всех трёх объектов-функций мы получим один и тот же результат:

$ python func.py 1.3
<type 'function'> : <function pow3 at 0xb7662844>
arg=1.3 => fun( arg )=2.197
<type 'function'> : <function <lambda> at 0xb7662bc4>
arg=1.3 => fun( arg )=2.197
<type 'function'> : <function <lambda> at 0xb7662844>
arg=1.3 => fun( arg )=2.197

Ещё более отчётливо это проявляется в Python версии 3, в которой всё является классами (в том числе, и целочисленная переменная), а функции являются объектами программы, принадлежащими к классу function:

$ python3 func.py 1.3
<class 'function'> : <function pow3 at 0xb74542ac>
arg=1.3 => fun( arg )=2.1970000000000005
<class 'function'> : <function <lambda> at 0xb745432c>
arg=1.3 => fun( arg )=2.1970000000000005
<class 'function'> : <function <lambda> at 0xb74542ec>
arg=1.3 => fun( arg )=2.1970000000000005

Примечание. Существуют ещё 2 типа объектов, допускающих функциональный вызов — функциональный метод класса и функтор, о которых мы поговорим позже.

Если функциональные объекты Python являются такими же объектами, как и другие объекты данных, значит, с ними можно и делать всё то, что можно делать с любыми данными:

  • динамически изменять в ходе выполнения;
  • встраивать в более сложные структуры данных (коллекции);
  • передавать в качестве параметров и возвращаемых значений и т.д.

На этом (манипуляции с функциональными объектами как с объектами данных) и базируется функциональное программирование. Python, конечно, не является настоящим языком функционального программирования, так, для полностью функционального программирования существуют специальные языки: Lisp, Planner, а из более свежих: Scala, Haskell. Ocaml, ... Но в Python можно "встраивать" приёмы функционального программирования в общий поток императивного (командного) кода, например, использовать методы, заимствованные из полноценных функциональных языков. Т.е. "сворачивать" отдельные фрагменты императивного кода (иногда достаточно большого объёма) в функциональные выражения.

Временами спрашивают: «В чём преимущества функционального стиля написания отдельных фрагментов для программиста?». Основным преимуществом функционального программирования является то, что после однократной отладки такого фрагмента в нём при последующем многократном использовании не возникнут ошибки за счёт побочных эффектов, связанных с присвоениями и конфликтом имён.

Достаточно часто при программировании на Python используют типичные конструкции из области функционального программирования, например:

print ( [ ( x,y ) for x in ( 1, 2, 3, 4, 5 ) \
                   for y in ( 20, 15, 10 ) \
                   if x * y > 25 and x + y < 25 ] )

В результате запуска получаем:

$ python funcp.py
[(2,20), (2,15), (3,20), (3,15), (3,10), (4,20), (4,15), (4,10), (5,15), (5,10)]

Функции как объекты

Создавая объект функции оператором 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
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
def input_arg():
    global arg
    arg = ( lambda: ( len( sys.argv ) > 1 and int( sys.argv[ 1 ] ) ) or \
                      int( input( "число?: " ) ) )()
    return arg

print( 'аргумент = {}'.format( input_arg() ) )
print( list( map( lambda x: x + 1, range( arg ) ) ) )
print( list( filter( lambda x: x > 4, range( arg ) ) ) )

import functools
print( '{}! = {}'.format( arg, functools.reduce( lambda x, y: x * y,
                                                 range( 1, arg ) ) ) )

Примечание. Этот код несколько усложнён по сравнению с предыдущим примером из-за следующих аспектов, связанных с совместимостью Python версий 2 и 3:

  • Функция reduce(), объявленная как встроенная в Python 2, в Python 3 была вынесена в модуль functools и её прямой вызов по имени вызовет исключение NameError, поэтому для корректной работы вызов должен быть оформлен как в примере или включать строку: from functools import *
  • Функции map() и filter() в Python 3 возвращают не список (что уже показывалось при обсуждении различий версий), а объекты-итераторы вида:
    <map object at 0xb7462bec>
    <filter object at 0xb75421ac>

Для получения всего списка значений для них вызывается функция list().

Поэтому такой код сможет работать в обеих версиях Python:

$ python3 funcH.py 7
аргумент = 7
[1, 2, 3, 4, 5, 6, 7]
[5, 6]
7! = 720

Если переносимость кода между различными версиями не требуется, то подобные фрагменты можно исключить, что позволит несколько упростить код.


Рекурсия

В функциональном программировании рекурсия является основным механизмом, аналогично циклам в итеративном программировании.

В некоторых обсуждениях по Python неоднократно приходилось встречаться с заявлениями, что в Python глубина рекурсии ограничена "аппаратно", и поэтому некоторые действия реализовать невозможно в принципе. В интерпретаторе Python действительно по умолчанию установлено ограничение глубины рекурсии, равным 1000, но это численный параметр, который всегда можно переустановить, как показано в листинге 3 (полный код примера можно найти в файле fact2.py из архива python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 3. Вычисление факториала с произвольной глубиной рекурсии
#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys

arg = lambda : ( len( sys.argv ) > 1 and int( sys.argv[ 1 ] ) ) or \
               int( input( "число?: " ) )

factorial = lambda x: ( ( x == 1 ) and 1 ) or x * factorial( x - 1 )

n = arg()
m = sys.getrecursionlimit()
if n >= m - 1 :
   sys.setrecursionlimit( n + 2 )
   print( "глубина рекурсии превышает установленную в системе {}, переустановлено в {}".\
          format( m, sys.getrecursionlimit() ) )

print( "n={} => n!={}".format( n, factorial( n ) ) )

if sys.getrecursionlimit() > m :
    print( "глубина рекурсии восстановлена в {}".format( m ) )
    sys.setrecursionlimit( m )

Вот как выглядит исполнение этого примера в Python 3 и в Python2 (правда на самом деле полученное число вряд ли поместится на один экран терминала консоли):

$ python3 fact2.py 1001
глубина рекурсии превышает установленную в системе 1000, переустановлено в 1003
n=1001 => n!=4027.................................................0000000000000
глубина рекурсии восстановлена в 1000

Несколько простейших примеров

Выполним несколько простейших трансформаций привычного императивного кода (командного, операторного) для превращения его отдельных фрагментов в функциональные. Сначала заменим операторы ветвления логическими условиями, которые за счёт "отложенных" (lazy, ленивых) вычислений позволяют управлять выполнением или невыполнением отдельных ветвей кода. Так, императивная конструкция:

if <условие>:
    <выражение 1>
else:
    <выражение 2>

- полностью эквивалентна следующему функциональному фрагменту (за счёт "отложенных" возможностей логических операторов and и or):

# функция без параметров:
lambda: ( <условие> and <выражение 1> ) or ( <выражение 2> )

В качестве примера снова используем вычисление факториала. В листинге 4 приведен функциональный код для вычисления факториала (файл fact1.py в архиве python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 4. Операторное (императивное) определение факториала
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
 
def factorial( n ):
    if n == 1: return 1
    else: return n * factorial( n - 1 )

if len( sys.argv ) > 1:
    n = int( sys.argv[ 1 ] )
else:
    n = int( input( "число?: " ) )

print( "n={} => n!={}".format( n, factorial( n ) ) )

Аргумент для вычисления извлекается из значения параметра командной строки (если он есть) или вводится с терминала. Первый вариант изменения, показанный выше, уже применяется в листинге 2, где на функциональные выражения были заменены:

  • определение функции факториала:
    factorial = lambda x: ( ( x == 1 ) and 1 ) or x * factorial( x - 1 )
  • запрос на ввод значения аргумента с консоли терминала:
    arg = lambda : ( len( sys.argv ) > 1 and int( sys.argv[ 1 ] ) ) or \
                     int( input( "число?: " ) )
    n = arg()

В файле fact3.py появляется ещё одно определение функции, сделанное через функцию высшего порядка reduсe():

factorial = factorial = lambda z: reduce( lambda x, y: x * y, range( 1, z + 1 ) )

Здесь же мы упростим также и выражение для n, сведя его к однократному вызову анонимной (не именованной) функции:

n = ( lambda : ( len( sys.argv ) > 1 and int( sys.argv[ 1 ] ) ) or \
                 int( input( "число?: " ) ) )()

Наконец, можно заметить, что присвоение значения переменной n требуется только для её использования в вызове print() для вывода этого значения. Если мы откажемся и от этого ограничения, то всё приложение выродится в один функциональный оператор (см. файл fact4.py в архиве python_functional.tgz в разделе "Материалы для скачивания"):

from sys import *
from functools import reduce
print( 'вычисленный факториал = {}'.format( \
         ( lambda z: reduce( lambda x, y: x * y, range( 1, z + 1 ) ) ) \
             ( ( lambda : ( len( argv ) > 1 and int( argv[ 1 ] ) ) or \
                 int( input( "число?: " ) ) )() ) ) )

Этот единственный вызов внутри функции print() и представляет всё приложение в его функциональном варианте:

$ python3 fact4.py
число?: 5
вычисленный факториал = 120

Читается ли этот код (файл fact4.py) лучше, чем императивная запись (файл fact1.py)? Скорее нет, чем да. В чём же тогда его достоинство? В том, что при любых изменениях окружающего его кода, нормальная работа этого фрагмента сохранится, так как отсутствует риск побочных эффектов из-за изменения значений используемых переменных.


Функции высших порядков

При функциональном стиле программирования стандартной практикой является динамическая генерация функционального объекта в процессе исполнения кода, с его последующим вызовом в том же коде. Существует целый ряд областей, где подобная техника может оказаться полезной.


Замыкание

Одно из интересных понятий функционального программирования - это замыкания (closure). Эта идея оказалась настолько заманчивой для многих разработчиков, что была реализована даже в некоторых нефункциональных языках программирования (Perl). Девид Мертц приводит следующее определение замыкания: "Замыкание - это процедура вместе с привязанной к ней совокупностью данных" (в противовес объектам в объектном программировании, как: "данные вместе с привязанным к ним совокупностью процедур" ).

Смысл замыкания состоит в том, что определение функции "замораживает" окружающий её контекст на момент определения. Это может делаться различными способами, например, за счёт параметризации создания функции, как показано в листинге 5 (файл clos1.py в архиве python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 5. Создание замыкания
# -*- coding: utf-8 -*-

def multiplier( n ):    # multiplier возвращает функцию умножения на n
    def mul( k ):
        return n * k
    return mul

mul3 = multiplier( 3 )  # mul3 - функция, умножающая на 3
print( mul3( 3 ), mul3( 5 ) )

Вот как срабатывает такая динамически определённая функция:

$ python clos1.py
(9, 15)
$ python3 clos1.py
9 15

Другой способ создания замыкания — это использование значения параметра по умолчанию в точке определения функции, как показано в листинге 6 (файл clos3.py из архива python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 6. Другой способ создания замыкания
n = 3
def mult( k, mul = n ):
    return mul * k

n = 7
print( mult( 3 ) )
n = 13
print( mult( 5 ) )

n = 10
mult = lambda k, mul=n: mul * k
print( mult( 3 ) )

Никакие последующие присвоения значений параметру по умолчанию не приведут к изменению ранее определённой функции, но сама функция может быть переопределена:

$ python clos3.py
9
15
30

Частичное применение функции

Частичное применение функции предполагает на основе функции N переменных определение новой функции с меньшим числом переменных M < N, при этом остальные N — M переменных получают фиксированные "замороженные" значения (используется модуль functools). Подобный пример будет рассмотрен ниже.


Функтор

Функтор — это не функция, а объект класса, в котором определён метод с именем __call__(). При этом, для экземпляра такого объекта может применяться вызов, точно так же, как это происходит для функций. В листинге 7 (файл part.py из архива python_functional.tgz в разделе "Материалы для скачивания") демонстрируется использование замыкания, частичного определения функции и функтора, приводящих к получению одного и того же результата.

Листинг 7. Сравнение замыкания, частичного определения и функтора
# -*- coding: utf-8 -*-

def multiplier( n ):    # замыкания - closure
    def mul( k ):
        return n * k
    return mul

mul3 = multiplier( 3 )

from functools import partial
def mulPart( a, b ):    # частичное применение функции
    return a * b

par3 = partial( mulPart, 3 )

class mulFunctor:       # эквивалентный функтор
    def __init__( self, val1 ):
        self.val1 = val1
    def __call__( self, val2 ):
        return self.val1 * val2

fun3 = mulFunctor( 3 )

print( '{} . {} . {}'.format( mul3( 5 ), par3( 5 ), fun3( 5 ) ) )

Вызов всех трёх конструкций для аргумента, равного 5, приведёт к получению одинакового результата, хотя при этом и будут использоваться абсолютно разные механизмы:

$ python part.py
15 . 15 . 15

Карринг

Карринг (или каррирование, curring) — преобразование функции от многих переменных в функцию, берущую свои аргументы по одному.

Примечание. Это преобразование было введено М. Шейнфинкелем и Г. Фреге и получило своё название в честь математика Хаскелла Карри, в честь которого также назван и язык программирования Haskell.

Карринг не относится к уникальным особенностям функционального программирования, так карринговое преобразование может быть записано, например, и на языках Perl или C++. Оператор каррирования даже встроен в некоторые языки программирования (ML, Haskell), что позволяет многоместные функции приводить к каррированному представлению. Но все языки, поддерживающие замыкания, позволяют записывать каррированные функции, и Python не является исключением в этом плане.

В листинге 8 представлен простейший пример с использованием карринга (файл curry1.py в архиве python_functional.tgz в разделе "Материалы для скачивания"):

Листинг 8. Карринг
# -*- coding: utf-8 -*-

def spam( x, y ):
    print( 'param1={}, param2={}'.format( x, y ) )

spam1 = lambda x : lambda y : spam( x, y )

def spam2( x ) :
    def new_spam( y ) :
        return spam( x, y )
    return new_spam

spam1( 2 )( 3 )      # карринг
spam2( 2 )( 3 )

Вот как выглядят исполнение этих вызовов:

$ python curry1.py
param1=2, param2=3
param1=2, param2=3

Заключение

В этой статье были представлены некоторые возможности языка Python, позволяющие применять его для написания программ, использующих стиль функционального программирования. Так, мы описали основные приёмы функционального программирования и показали примеры их реализации в Python. Как и в предыдущих статьях, примеры кода написаны таким образом, что могут успешно запускаться и исполняться в обеих версиях Python.

В следующей статье мы обсудим вопросы организации параллельного исполнения кода в среде Python.


Загрузка

ОписаниеИмяРазмер
особенности типизации в Pythonpython_types.tgz2KB

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


  • Bluemix

    Узнайте больше информации о платформе IBM Bluemix, создавайте приложения, используя готовые решения!

  • developerWorks Premium

    Эксклюзивные инструменты для построения вашего приложения. Узнать больше.

  • Библиотека документов

    Более трех тысяч статей, обзоров, руководств и других полезных материалов.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=954101
ArticleTitle=Тонкости использования языка Python: Часть 3. Функциональное программирование
publish-date=11222013