Содержание


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

Comments

Существует большое количество публикаций, посвящённых реализациям концепций функционального программирования на языке 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.


Ресурсы для скачивания


Похожие темы


Комментарии

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

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