Функции в питоне. Функции в программировании. Функции в Python

5 августа 2008 в 16:14

Основы Python - кратко. Часть 5. Определение функций, основы.

  • Python

Начав писать главу про ООП, понял что совсем забыл освятить такой большой и нужный раздел Пайтона как функции. Тема это большая и обширная, потому, чтобы не сильно растягивать паузу между уроками, решил разделить ее на 2 части. Сначала расскажу основы, потом уже углубленные особенности Пайтоновского функциестроения.

Функции в Пайтоне объявляются не просто, а очень просто. Вот пример самой простой:

Def empty_func(): pass
Начинается объявление с ключевого слова def, что как не сложно догадаться является сокращением от define. После него идет имя функции. После имени в круглых скобках задается список параметров, в данном случае отсутствующих.
Тело функции пишется с отступом со следующей строки. учтите, что в Пайтоне функции с пустым телом запрещены, потому в качестве тела приведенной выше функции используется «пустой оператор» pass.
Теперь рассмотрим пример посерьезнее.

Def safe_div(x, y): """Do a safe division:-) for fun and profit""" if y != 0: z = x / y print z return z else: print "Yippie-kay-yay, motherf___er!"
В этом примере есть несколько нововведений. первое, что бросается в глаза - это строка документации (docstring), идущая сразу после тела функции.
Обычно эта строка занимает не одну строку исходного текста (простите за каламбур) и потому задается в тройных кавычках. Она предназначена для описания функции, ее предназначения, параметров и т.п. Все хорошие ИДЕ умеют с этой строкой работать. Получить к ней доступ можно и из самой программы, используя свойство __doc__:

Print safe_div.__doc__
Этим свойством (да, да, именно свойством, в Пайтоне даже функции на самом деле - классы) удобно пользоваться во время сеансов работы интерактивной консоли.
>>> from ftplib import FTP >>> print FTP.__doc__ An FTP client class. To create a connection, call the class using these argument: host, user, passwd, acct These are all strings, and have default value "". Then use self.connect() with optional host and port argument. # дальнейшее почикано мною:-)
Вернемся к нашей исходной функции. Суть ее очень проста, она принимает 2 параметра: х и у. Если у не равен 0, она делит х на у, выводит результат на экран и возвращает свое частное в виде результата. Результат функции возвращают с помощью команды return. Благодаря механизму кортежей, описанному в прошлом уроке, функции в Пайтоне могут возвращать одновременно множество объектов.
Если же делитель все-таки равен нулю, функция выводит сообщение об ошибке. Неверно было бы предположить что в этом случае функция ничего не вернет. Правильнее будет сказать что функция вернет «ничего»:) Иначе говоря, если в функции отсутствует оператор return, или же он вызван без параметров, то функция возвращает специальное значение None. В этом легко убедиться вызвав что-то типа print safe_div(10, 0).

Вот пример слегка посложнее, он взят из доклада-презентации Гвидо ван Россума.

Def gcd(a, b): "Нахождение НОД" while a != 0: a,b = b%a,a # параллельное определение return b
Данная функция находит наибольший общий делитель двух чисел.

В общем, следует учитывать, что параметры в функции Пайтоном передаются по ссылке. Еще одним, возможно нетривиальным фактом к которому придется привыкать - является тот факт что сами функции являются значением, которое можно присваивать. Если воспользоваться нашей функцией safe_div для дальнейших экспериментов, то можно написать следующий код.

Mystic_function = safe_div print mystic_function(10, 4)
Вот на этот раз и все, «за бортом» осталось еще много аспектов определения функций в Пайтоне, которые будут освещены в следующий раз.

Упражнения для проверки.
1. На основе существующей функции нахождения НОД, напишите функцию поиска НОК двух чисел.
2. Напишите подпрограмму табулирования функции, переданной в качестве аргумента. Так же аргументами задается начальное, конечное значение и шаг табуляции.

PS кстати, каков оптимальный объем «урока»? Что лучше - реже выходящие большие главы, или «лучше меньше да чаще».

Напомним, что в математике факториал числа n определяется как n! = 1 ⋅ 2 ⋅ ... ⋅ n. Например, 5! = 1 ⋅ 2 ⋅ 3 ⋅ 4 ⋅ 5 = 120. Ясно, что факториал можно легко посчитать, воспользовавшись циклом for. Представим, что нам нужно в нашей программе вычислять факториал разных чисел несколько раз (или в разных местах кода). Конечно, можно написать вычисление факториала один раз, а затем используя Copy-Paste вставить его везде, где это будет нужно.

# вычислим 3! res = 1 for i in range(1, 4): res *= i print(res) # вычислим 5! res = 1 for i in range(1, 6): res *= i print(res)

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

Функции — это такие участки кода, которые изолированы от остальный программы и выполняются только тогда, когда вызываются. Вы уже встречались с функциями sqrt(), len() и print(). Они все обладают общим свойством: они могут принимать параметры (ноль, один или несколько), и они могут возвращать значение (хотя могут и не возвращать). Например, функция sqrt() принимает один параметр и возвращает значение (корень числа). Функция print() принимает переменное число параметров и ничего не возвращает.

Покажем, как написать функцию factorial(), которая принимает один параметр — число, и возвращает значение — факториал этого числа.

Def factorial(n): res = 1 for i in range(1, n + 1): res *= i return res print(factorial(3)) print(factorial(5))

Дадим несколько объяснений. Во-первых, код функции должен размещаться в начале программы, вернее, до того места, где мы захотим воспользоваться функцией factorial(). Первая строчка этого примера является описанием нашей функции. factorial - идентификатор, то есть имя нашей функции. После идентификатора в круглых скобках идет список параметров, которые получает наша функция. Список состоит из перечисленных через запятую идентификаторов параметров. В нашем случае список состоит из одной величины n. В конце строки ставится двоеточие.

Далее идет тело функции, оформленное в виде блока, то есть с отступом. Внутри функции вычисляется значение факториала числа n и оно сохраняется в переменной res. Функция завершается инструкцией return res, которая завершает работу функции и возвращает значение переменной res.

Инструкция return может встречаться в произвольном месте функции, ее исполнение завершает работу функции и возвращает указанное значение в место вызова. Если функция не возвращает значения, то инструкция return используется без возвращаемого значения. В функциях, которым не нужно возвращать значения, инструкция return может отсутствовать.

Приведём ещё один пример. Напишем функцию max(), которая принимает два числа и возвращает максимальное из них (на самом деле, такая функция уже встроена в Питон).

10 20 def max(a, b): if a > b: return a else: return b print(max(3, 5)) print(max(5, 3)) print(max(int(input()), int(input())))

Теперь можно написать функцию max3(), которая принимает три числа и возвращает максимальное их них.

Def max(a, b): if a > b: return a else: return b def max3(a, b, c): return max(max(a, b), c) print(max3(3, 5, 4))

Встроенная функция max() в Питоне может принимать переменное число аргументов и возвращать максимум из них. Приведём пример того, как такая функция может быть написана.

Def max(*a): res = a for val in a: if val > res: res = val return res print(max(3, 5, 4))

Все переданные в эту функцию параметры соберутся в один кортеж с именем a, на что указывает звёздочка в строке объявления функции.

2. Локальные и глобальные переменные

Внутри функции можно использовать переменные, объявленные вне этой функции

Def f(): print(a) a = 1 f()

Здесь переменной a присваивается значение 1, и функция f() печатает это значение, несмотря на то, что до объявления функции f эта переменная не инициализируется. В момент вызова функции f() переменной a уже присвоено значение, поэтому функция f() может вывести его на экран.

Такие переменные (объявленные вне функции, но доступные внутри функции) называются глобальными .

Но если инициализировать какую-то переменную внутри функции, использовать эту переменную вне функции не удастся. Например:

Def f(): a = 1 f() print(a)

Получим ошибку NameError: name "a" is not defined . Такие переменные, объявленные внутри функции, называются локальными . Эти переменные становятся недоступными после выхода из функции.

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

Def f(): a = 1 print(a) a = 0 f() print(a)

Будут выведены числа 1 и 0. Несмотря на то, что значение переменной a изменилось внутри функции, вне функции оно осталось прежним! Это сделано в целях “защиты” глобальных переменных от случайного изменения из функции. Например, если функция будет вызвана из цикла по переменной i , а в этой функции будет использована переменная i также для организации цикла, то эти переменные должны быть различными. Если вы не поняли последнее предложение, то посмотрите на следующий код и подумайте, как бы он работал, если бы внутри функции изменялась переменная i.

Def factorial(n): res = 1 for i in range(1, n + 1): res *= i return res for i in range(1, 6): print(i, "! = ", factorial(i), sep="")

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

5! = 1 5! = 2 5! = 6 5! = 24 5! = 120

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

Более формально: интерпретатор Питон считает переменную локальной для данной функции, если в её коде есть хотя бы одна инструкция, модифицирующая значение переменной, то эта переменная считается локальной и не может быть использована до инициализации. Инструкция, модифицирующая значение переменной — это операторы = , += , а также использование переменной в качестве параметра цикла for . При этом даже если инструкция, модицифицирующая переменную никогда не будет выполнена, интерпретатор это проверить не может, и переменная все равно считается локальной. Пример:

Def f(): print(a) if False: a = 0 a = 1 f()

Возникает ошибка: UnboundLocalError: local variable "a" referenced before assignment . А именно, в функции f() идентификатор a становится локальной переменной, т.к. в функции есть команда, модифицирующая переменную a , пусть даже никогда и не выполняющийся (но интерпретатор не может это отследить). Поэтому вывод переменной a приводит к обращению к неинициализированной локальной переменной.

Чтобы функция могла изменить значение глобальной переменной, необходимо объявить эту переменную внутри функции, как глобальную, при помощи ключевого слова global:

Def f(): global a a = 1 print(a) a = 0 f() print(a)

В этом примере на экран будет выведено 1 1, так как переменная a объявлена, как глобальная, и ее изменение внутри функции приводит к тому, что и вне функции переменная будет доступна.

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

Например, пусть ваша программа должна посчитать факториал вводимого числа, который вы потом захотите сохранить в переменной f. Вот как это не стоит делать:

5 def factorial(n): global f res = 1 for i in range(2, n + 1): res *= i f = res n = int(input()) factorial(n) # дальше всякие действия с переменной f

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

Гораздо лучше переписать этот пример так:

5 # начало куска кода, который можно копировать из программы в программу def factorial(n): res = 1 for i in range(2, n + 1): res *= i return res # конец куска кода n = int(input()) f = factorial(n) # дальше всякие действия с переменной f

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

Тогда результат вызова функции можно будет использовать во множественном присваивании:

3. Рекурсия

Def short_story(): print("У попа была собака, он ее любил.") print("Она съела кусок мяса, он ее убил,") print("В землю закопал и надпись написал:") short_story()

Как мы видели выше, функция может вызывать другую функцию. Но функция также может вызывать и саму себя! Рассмотрим это на примере функции вычисления факториала. Хорошо известно, что 0!=1, 1!=1. А как вычислить величину n! для большого n? Если бы мы могли вычислить величину (n-1)!, то тогда мы легко вычислим n!, поскольку n!=n⋅(n-1)!. Но как вычислить (n-1)!? Если бы мы вычислили (n-2)!, то мы сможем вычисли и (n-1)!=(n-1)⋅(n-2)!. А как вычислить (n-2)!? Если бы... В конце концов, мы дойдем до величины 0!, которая равна 1. Таким образом, для вычисления факториала мы можем использовать значение факториала для меньшего числа. Это можно сделать и в программе на Питоне:

Def factorial(n): if n == 0: return 1 else: return n * factorial(n - 1) print(factorial(5))

Подобный прием (вызов функцией самой себя) называется рекурсией, а сама функция называется рекурсивной.

Рекурсивные функции являются мощным механизмом в программировании. К сожалению, они не всегда эффективны. Также часто использование рекурсии приводит к ошибкам. Наиболее распространенная из таких ошибок - бесконечная рекурсия, когда цепочка вызовов функций никогда не завершается и продолжается, пока не кончится свободная память в компьютере. Пример бесконечной рекурсии приведен в эпиграфе к этому разделу. Две наиболее распространенные причины для бесконечной рекурсии:

  1. Неправильное оформление выхода из рекурсии. Например, если мы в программе вычисления факториала забудем поставить проверку if n == 0 , то factorial(0) вызовет factorial(-1) , тот вызовет factorial(-2) и т. д.
  2. Рекурсивный вызов с неправильными параметрами. Например, если функция factorial(n) будет вызывать factorial(n) , то также получится бесконечная цепочка.

Поэтому при разработке рекурсивной функции необходимо прежде всего оформлять условия завершения рекурсии и думать, почему рекурсия когда-либо завершит работу.

Функции могут передавать какие-либо данные из своих тел в основную ветку программы. Говорят, что функция возвращает значение. В большинстве языков программирования, в том числе Python, выход из функции и передача данных в то место, откуда она была вызвана, выполняется оператором return.

Если интерпретатор Питона, выполняя тело функции, встречает return, то он "забирает" значение, указанное после этой команды, и "уходит" из функции.

def cylinder() : r = float (input () ) h = float (input () ) # площадь боковой поверхности цилиндра: side = 2 * 3.14 * r * h # площадь одного основания цилиндра: circle = 3.14 * r**2 # полная площадь цилиндра: full = side + 2 * circle return full square = cylinder() print (square)

Пример выполнения:

3 7 188.4

В данной программе в основную ветку из функции возвращается значение локальной переменной full . Не сама переменная, а ее значение, в данном случае – какое-либо число, полученное в результате вычисления площади цилиндра.

В основной ветке программы это значение присваивается глобальной переменной square . То есть выражение square = cylinder() выполняется так:

    Вызывается функция cylinder() .

    Из нее возвращается значение.

    Это значение присваивается переменной square .

Не обязательно присваивать результат переменной, его можно сразу вывести на экран:

Print (cylinder() )

Здесь число, полученное из cylinder(), непосредственно передается функции print(). Если мы в программе просто напишем cylinder(), не присвоив полученные данные переменной или не передав их куда-либо дальше, то эти данные будут потеряны. Но синтаксической ошибки не будет.

В функции может быть несколько операторов return. Однако всегда выполняется только один из них. Тот, которого первым достигнет поток выполнения. Допустим, мы решили обработать исключение, возникающее на некорректный ввод. Пусть тогда в ветке except обработчика исключений происходит выход из функции без всяких вычислений и передачи значения:

def cylinder() : try : r = float (input () ) h = float (input () ) except ValueError : return side = 2 * 3.14 * r * h circle = 3.14 * r**2 full = side + 2 * circle return full print (cylinder() )

Если попытаться вместо цифр ввести буквы, то сработает return, вложенный в except. Он завершит выполнение функции, так что все нижеследующие вычисления, в том числе return full, будут опущены. Пример выполнения:

Но постойте! Что это за слово None, которое нам вернул "пустой" return? Это ничего, такой объект – "ничто". Он принадлежит классу NoneType. До этого мы знали четыре типа данных, они же четыре класса: int, float, str, bool. Пришло время пятого.

Когда после return ничего не указывается, то по умолчанию считается, что там стоит объект None. Но никто вам не мешает явно написать return None.

Более того. Ранее мы рассматривали функции, которые вроде бы не возвращали никакого значения, потому что в них не было оператора return. На самом деле возвращали, просто мы не обращали на него внимание, не присваивали никакой переменной и не выводили на экран. В Python всякая функция что-либо возвращает. Если в ней нет оператора return, то она возвращает None. То же самое, как если в ней имеется "пустой" return.

Возврат нескольких значений

В Питоне позволительно возвращать из функции несколько объектов, перечислив их через запятую после команды return:

def cylinder() : r = float (input () ) h = float (input () ) side = 2 * 3.14 * r * h circle = 3.14 * r**2 full = side + 2 * circle return side, full sCyl, fCyl = cylinder() print ("Площадь боковой поверхности %.2f" % sCyl) print ("Полная площадь %.2f" % fCyl)

Из функции cylinder() возвращаются два значения. Первое из них присваивается переменной sCyl , второе – fCyl . Возможность такого группового присвоения – особенность Python, обычно не характерная для других языков:

>>> a, b, c = 10 , 15 , 19 >>> a 10 >>> b 15 >>> c 19

Фокус здесь в том, что перечисление значений через запятую (например, 10, 15, 19) создает объект типа tuple. На русский переводится как "кортеж". Это разновидность структур данных, которые будут изучены позже.

Когда же кортеж присваивается сразу нескольким переменным, то происходит сопоставление его элементов соответствующим в очереди переменным. Это называется распаковкой.

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

Распаковка не является обязательной. Будет работать и так:

… print (cylinder() )

Пример выполнения:

4 3 (75.36 , 175.84 )

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

Существует большое количество публикаций, посвящённых реализациям концепций функционального программирования на языке 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 : arg=1.3 => fun(arg)=2.197 : at 0xb7662bc4> arg=1.3 => fun(arg)=2.197 : at 0xb7662844> arg=1.3 => fun(arg)=2.197

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

$ python3 func.py 1.3 : arg=1.3 => fun(arg)=2.1970000000000005 : at 0xb745432c> arg=1.3 => fun(arg)=2.1970000000000005 : 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 возвращают не список (что уже показывалось при обсуждении различий версий), а объекты-итераторы вида:

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

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

$ python3 funcH.py 7 аргумент = 7 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.

В этой статье я планирую рассказать о функциях, именных и анонимных, инструкциях def, return и lambda, обязательных и необязательных аргументах функции, функциях с произвольным числом аргументов.

Именные функции, инструкция def

Функция в python - объект, принимающий аргументы и возвращающий значение. Обычно функция определяется с помощью инструкции def .

Определим простейшую функцию:

def add (x , y ): return x + y

Инструкция return говорит, что нужно вернуть значение. В нашем случае функция возвращает сумму x и y.

Теперь мы ее можем вызвать:

>>> add (1 , 10 ) 11 >>> add ("abc" , "def" ) "abcdef"

Функция может быть любой сложности и возвращать любые объекты (списки, кортежи, и даже функции!):

>>> def newfunc (n ): ... def myfunc (x ): ... return x + n ... return myfunc ... >>> new = newfunc (100 ) # new - это функция >>> new (200 ) 300

Функция может и не заканчиваться инструкцией return, при этом функция вернет значение :

>>> def func (): ... pass ... >>> print (func ()) None

Аргументы функции

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

>>> def func (a , b , c = 2 ): # c - необязательный аргумент ... return a + b + c ... >>> func (1 , 2 ) # a = 1, b = 2, c = 2 (по умолчанию) 5 >>> func (1 , 2 , 3 ) # a = 1, b = 2, c = 3 6 >>> func (a = 1 , b = 3 ) # a = 1, b = 3, c = 2 6 >>> func (a = 3 , c = 6 ) # a = 3, c = 6, b не определен Traceback (most recent call last): File "", line 1, in func(a=3, c=6) TypeError : func() takes at least 2 arguments (2 given)

Функция также может принимать переменное количество позиционных аргументов, тогда перед именем ставится *:

>>> def func (* args ): ... return args ... >>> func (1 , 2 , 3 , "abc" ) (1, 2, 3, "abc") >>> func () () >>> func (1 ) (1,)

Как видно из примера, args - это из всех переданных аргументов функции, и с переменной можно работать также, как и с кортежем.

Функция может принимать и произвольное число именованных аргументов, тогда перед именем ставится **:

>>> def func (** kwargs ): ... return kwargs ... >>> func (a = 1 , b = 2 , c = 3 ) {"a": 1, "c": 3, "b": 2} >>> func () {} >>> func (a = "python" ) {"a": "python"}

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

Анонимные функции, инструкция lambda

Анонимные функции могут содержать лишь одно выражение, но и выполняются они быстрее. Анонимные функции создаются с помощью инструкции lambda . Кроме этого, их не обязательно присваивать переменной, как делали мы инструкцией def func():

>>> func = lambda x , y : x + y >>> func (1 , 2 ) 3 >>> func ("a" , "b" ) "ab" >>> (lambda x , y : x + y )(1 , 2 ) 3 >>> (lambda x , y : x + y )("a" , "b" ) "ab"

lambda функции, в отличие от обычной, не требуется инструкция return, а в остальном, ведет себя точно так же:

>>> func = lambda * args : args >>> func (1 , 2 , 3 , 4 ) (1, 2, 3, 4)