Введение в функциональное программирование на Python. Функциональное программирование на Python

2010-11-17 09:47

Функции map, zip и лямбда (кстати говоря называются "функции высшего порядка" или "first-class-functions") позволяют достаточно просто выполнять различные манипуляции с данными, для чего в "обычном" процедурном стиле приходится писать немного больше кода. Все ниженаписанное относится к так называемому функциональному программированию , луркайте подробности.

Функции map, zip и lambda в примерах.

Простая задача есть список a = и список b = одинаковой длины и нужно слить их парами. Проще простого - используя функцию zip :

a = [ 1 , 2 ] b = [ 3 , 4 ] print zip (a , b ) [(1 , 3 ), (2 , 4 )]

или тройками:

a = [ 1 , 2 ] b = [ 3 , 4 ] c = [ 5 , 6 ] print zip (a , b , c ) [(1 , 3 , 5 ), (2 , 4 , 6 )]

или в более общем виде

list = [ a , b , c ] print zip (* list ) [(1 , 3 , 5 ), (2 , 4 , 6 )]

Звездочка * перед list как-бы говорит что передается список аргументов, т.е. Действовать эквивалентно тому как если бы передали a, b, c т.е. Можно даже так print zip(*) результат не изменится.

def f (x ): return x * x nums = [ 1 , 2 , 3 ] for num in nums : print f (num )

Более опытный нуб изучивший list comprehensions:

def f (x ): return x * x print [ f (num ) for num in nums ]

Программист сделает проще:

def f (x ): return x * x print map (f , nums )

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

print map (lambda x : x * x , nums )

Последняя запись являет собой пример наиболее грамотного подхода. Дело в том, что когда человек пишет код как стихи, в порыве вдохновения (что другими словами можно назвать "в диком угаре"), крайне роляет скорость написания (отсюда растут корни трепетной любви многих девелоперов к простым текстовым редакторм vim, emacs, sublimetext), а сильная сторона питона как раз в размере генерируемого кода - он очень компактный. Написать одну строчку естественно быстрее чем 7, да и читать короткий код проще, однако написание подобного кода требует определенного навыка. Другая сторона медали – иногда в этом "диком угаре" пишут в одну строчку целые последовательности достаточно сложных действий, да так что очень трудно понять что там происходит и что получается в конечном итоге.

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

def f (x , y ): return x * y a = [ 1 , 3 , 4 ] b = [ 3 , 4 , 5 ] print map (f , a , b ) [ 3 , 12 , 20 ]

Классно, правда?

Однако если списки разной длины, т.е. Один короче другого, то он будет дополнен значениями None до нужной длины. Если убрать из списка b последнее значение – пример не будет работать, т.к. В функции f произойдет попытка умножения числа на None, и питоне не позволяет это делать, что кстати выгодно отличает его от php, который в подобной ситуации работал бы дальше. Поэтому если функция f достаточно объемна, неплохо бы проверять передаваемые значения. Например;

Если же заместо функции стоит None – то map действует примерно так же как и zip , но если передаваемые списки разной длины в результат будет писаться None – что кстати очень уместно в некоторых моментах.

a = [ 1 , 3 , 4 ] b = [ 3 , 4 ] print map (None , a , b ) [(1 , 3 ), (3 , 4 ), (4 , None )]

Теперь про лямбда функции в python . Они используются когда вам необходимо определить функцию без исподьзования def func_name(): ..., ведь часто (как в предыдущих примерах) функция настолько мала, что определять её отдельно смыла нет (лишние строчки кода, что ухудшение читабельность). Поэтому функцию можно определить “на месте” f = lambda x: x*x как бы говорит нам – принимает x, возвращает x*x

Так используя стандартные инструменты питона можно записать довольно сложные действия в одну строчку. К примеру функцию:

def f (x , y ): if (y == None ): y = 1 return x * y

можно представить как:

lambda x , y : x * (y if y is not None else 1 )

А теперь хорошо бы передавать списки отсортированные по длине – len(a) > (b) – проще простого - воспользуемся функцией sorted :

sorted ([ a , b ], key = lambda x : len (x ), reverse = True )

фунция sorted принимает список значений ( = [,]) и сортирует по ключу key – который у нас задан функцией len(x) - возвращающей длину списка, сортируем в порядке убывания (reverse=True)

В конечном итоге вся операция записывается таким образом:

map (lambda x , y : x * (y if y is not None else 1 ), * sorted ([ a , b ], key = lambda x : len (x ), reverse = True ))

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

  • Перевод

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

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

Нефункциональная функция:

A = 0 def increment1(): global a a += 1

Функциональная функция:

Def increment2(a): return a + 1

Вместо проходов по списку используйте map и reduce

Map

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

Простой map, принимающий список имён и возвращающий список длин:

Name_lengths = map(len, ["Маша", "Петя", "Вася"]) print name_lengths # =>

Этот map возводит в квадрат каждый элемент:

Squares = map(lambda x: x * x, ) print squares # =>

Он не принимает именованную функцию, а берёт анонимную, определённую через lambda. Параметры lambda определены слева от двоеточия. Тело функции – справа. Результат возвращается неявным образом.

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

Import random names = ["Маша", "Петя", "Вася"] code_names = ["Шпунтик", "Винтик", "Фунтик"] for i in range(len(names)): names[i] = random.choice(code_names) print names # => ["Шпунтик", "Винтик", "Шпунтик"]

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

Перепишем это через map:

Import random names = ["Маша", "Петя", "Вася"] secret_names = map(lambda x: random.choice(["Шпунтик", "Винтик", "Фунтик"]), names)

Упражнение 1 . Попробуйте переписать следующий код через map. Он принимает список реальных имён и заменяет их прозвищами, используя более надёжный метод.

Names = ["Маша", "Петя", "Вася"] for i in range(len(names)): names[i] = hash(names[i]) print names # =>

Моё решение:

names = ["Маша", "Петя", "Вася"] secret_names = map(hash, names)

Reduce

Reduce принимает функцию и набор пунктов. Возвращает значение, получаемое комбинированием всех пунктов.

Пример простого reduce. Возвращает сумму всех пунктов в наборе:

Sum = reduce(lambda a, x: a + x, ) print sum # => 10

X – текущий пункт, а – аккумулятор. Это значение, которое возвращает выполнение lambda на предыдущем пункте. reduce() перебирает все значения, и запускает для каждого lambda на текущих значениях а и х, и возвращает результат в а для следующей итерации.

А чему равно а в первой итерации? Оно равно первому элементу коллекции, и reduce() начинает работать со второго элемента. То есть, первый х будет равен второму предмету набора.

Следующий пример считает, как часто слово «капитан» встречается в списке строк:

Sentences = ["капитан джек воробей", "капитан дальнего плавания", "ваша лодка готова, капитан"] cap_count = 0 for sentence in sentences: cap_count += sentence.count("капитан") print cap_count # => 3

Тот же код с использованием reduce:

Sentences = ["капитан джек воробей", "капитан дальнего плавания", "ваша лодка готова, капитан"] cap_count = reduce(lambda a, x: a + x.count("капитан"), sentences, 0)

А откуда здесь берётся начальное значение а? Оно не может быть вычислено из количества повторений в первой строке. Поэтому оно задаётся как третий аргумент функции reduce().

Почему map и reduce лучше?

Во-первых, они обычно укладываются в одну строку.

Во-вторых, важные части итерации,– коллекция, операция и возвращаемое значение,– всегда находятся в одном месте map и reduce.

В-третьих, код в цикле может изменить значение ранее определённых переменных, или влиять на код, находящийся после него. По соглашению, map и reduce – функциональны.

В-четвёртых, map и reduce – элементарные операции. Вместо построчного чтения циклов читателю проще воспринимать map и reduce, встроенные в сложные алгоритмы.

В-пятых, у них есть много друзей, позволяющих полезное, слегка изменённое поведение этих функций. Например, filter, all, any и find.

Упражнение 2 : перепишите следующий код, используя map, reduce и filter. Filter принимает функцию и коллекцию. Возвращает коллекцию тех вещей, для которых функция возвращает True.

People = [{"имя": "Маша", "рост": 160}, {" рост ": "Саша", " рост ": 80}, {"name": "Паша"}] height_total = 0 height_count = 0 for person in people: if "рост" in person: height_total += person[" рост "] height_count += 1 if height_count > 0: average_height = height_total / height_count print average_height # => 120

Моё решение:

people = [{"имя": "Маша", "рост": 160}, {" рост ": "Саша", " рост ": 80}, {"name": "Паша"}] heights = map(lambda x: x["рост"], filter(lambda x: "рост" in x, people)) if len(heights) > 0: from operator import add average_height = reduce(add, heights) / len(heights)

Пишите декларативно, а не императивно

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

Примеры вывода:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Текст программы:

From random import random time = 5 car_positions = while time: # decrease time time -= 1 print "" for i in range(len(car_positions)): # move car if random() > 0.3: car_positions[i] += 1 # draw car print "-" * car_positions[i]

Код императивен. Функциональная версия была бы декларативной – она бы описывала, что нужно сделать, а не то, как это надо сделать.

Используем функции

Декларативности можно достичь, вставляя код в функции:

From random import random def move_cars(): for i, _ in enumerate(car_positions): if random() > 0.3: car_positions[i] += 1 def draw_car(car_position): print "-" * car_position def run_step_of_race(): global time time -= 1 move_cars() def draw(): print "" for car_position in car_positions: draw_car(car_position) time = 5 car_positions = while time: run_step_of_race() draw()

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

Комментарии не нужны, код объясняет сам себя.

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

Вот функциональная версия этой программы:

From random import random def move_cars(car_positions): return map(lambda x: x + 1 if random() > 0.3 else x, car_positions) def output_car(car_position): return "-" * car_position def run_step_of_race(state): return {"time": state["time"] - 1, "car_positions": move_cars(state["car_positions"])} def draw(state): print "" print "\n".join(map(output_car, state["car_positions"])) def race(state): draw(state) if state["time"]: race(run_step_of_race(state)) race({"time": 5, "car_positions": })

Теперь код разбит на функциональные функции. Тому есть три признака. Первый – нет расшаренных переменных. time и car_positions передаются прямиком в race(). Второе – функции принимают параметры. Третье – переменные не меняются внутри функций, все значения возвращаются. Каждый раз, когда run_step_of_race() проделывает следующий шаг, он передаётся опять в следующий.

Вот вам две функции zero() и one():

Def zero(s): if s == "0": return s def one(s): if s == "1": return s

Zero() принимает строку s. Если первый символ – 0, то возвращает остаток строки. Если нет – тогда None. one() делает то же самое, если первый символ – 1.

Представим функцию rule_sequence(). Она принимает строку и список из функций-правил, состоящий из функций zero и one. Она вызывает первое правило, передавая ему строку. Если не возвращено None, то берёт возвращённое значение и вызывает следующее правило. И так далее. Если возвращается None, rule_sequence() останавливается и возвращает None. Иначе – значение последнего правила.

Примеры входных и выходных данных:

Print rule_sequence("0101", ) # => 1 print rule_sequence("0101", ) # => None

Императивная версия rule_sequence():

Def rule_sequence(s, rules): for rule in rules: s = rule(s) if s == None: break return s

Упражнение 3 . Этот код использует цикл. Перепишите его в декларативном виде с использованием рекурсии.

Моё решение:

def rule_sequence(s, rules): if s == None or not rules: return s else: return rule_sequence(rules(s), rules)

Используйте конвейеры (pipelines)

Теперь перепишем другой вид циклов при помощи приёма под названием конвейер.

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

Bands = [{"name": "sunset rubdown", "country": "UK", "active": False}, {"name": "women", "country": "Germany", "active": False}, {"name": "a silver mt. zion", "country": "Spain", "active": True}] def format_bands(bands): for band in bands: band["country"] = "Canada" band["name"] = band["name"].replace(".", "") band["name"] = band["name"].title() format_bands(bands) print bands # => [{"name": "Sunset Rubdown", "active": False, "country": "Canada"}, # {"name": "Women", "active": False, "country": "Canada" }, # {"name": "A Silver Mt Zion", "active": True, "country": "Canada"}]

Название функции «format» слишком общее. И вообще, код вызывает некоторое беспокойство. В одном цикле происходят три разные вещи. Значение ключа "country" меняется на "Canada". Убираются точки и первая буква имени меняется на заглавную. Сложно понять, что код должен делать, и сложно сказать, делает ли он это. Его тяжело использовать, тестировать и распараллеливать.

Сравните:

Print pipeline_each(bands, )

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

Pipeline_each() перебирает группы по одной, и передаёт их функциям преобразования, вроде set_canada_as_country(). После применения функции ко всем группам, pipeline_each() делает из них список и передаёт следующей.

Посмотрим на функции преобразования.

Def assoc(_d, key, value): from copy import deepcopy d = deepcopy(_d) d = value return d def set_canada_as_country(band): return assoc(band, "country", "Canada") def strip_punctuation_from_name(band): return assoc(band, "name", band["name"].replace(".", "")) def capitalize_names(band): return assoc(band, "name", band["name"].title())

Каждая связывает ключ группы с новым значением. Без изменения оригинальных данных это тяжело сделать, поэтому мы решаем это с помощью assoc(). Она использует deepcopy() для создания копии переданного словаря. Каждая функция преобразовывает копию и возвращает эту копию.

Всё вроде как нормально. Оригиналы данных защищены от изменений. Но в коде есть два потенциальных места для изменений данных. В strip_punctuation_from_name() создаётся имя без точек через вызов calling replace() с оригинальным именем. В capitalize_names() создаётся имя с первой прописной буквой на основе title() и оригинального имени. Если replace и time не функциональны, то и strip_punctuation_from_name() с capitalize_names() не функциональны.

К счастью, они функциональны. В Python строки неизменяемы. Эти функции работают с копиями строк. Уфф, слава богу.

Такой контраст между строками и словарями (их изменяемостью) в Python демонстрирует преимущества языков типа Clojure. Там программисту не надо думать, не изменит ли он данные. Не изменит.

Упражнение 4 . Попробуйте сделать функцию pipeline_each. Задумайтесь над последовательностью операций. Группы – в массиве, передаются по одной для первой функции преобразования. Затем полученный массив передаётся по одной штучке для второй функции, и так далее.

Моё решение:

def pipeline_each(data, fns): return reduce(lambda a, x: map(x, a), fns, data)

Все три функции преобразования в результате меняют конкретное поле у группы. call() можно использовать, чтобы создать абстракцию для этого. Она принимает функцию и ключ, к которому она будет применена.

Set_canada_as_country = call(lambda x: "Canada", "country") strip_punctuation_from_name = call(lambda x: x.replace(".", ""), "name") capitalize_names = call(str.title, "name") print pipeline_each(bands, )

Или, жертвуя читаемостью:

Print pipeline_each(bands, )

Код для call():

Def assoc(_d, key, value): from copy import deepcopy d = deepcopy(_d) d = value return d def call(fn, key): def apply_fn(record): return assoc(record, key, fn(record.get(key))) return apply_fn

Что тут у нас происходит.

Один. call – функция высшего порядка, т.к. принимает другую функцию как аргумент и возвращает функцию.

Два. apply_fn() похожа на функции преобразования. Получает запись (группу). Ищет значение record. Вызывает fn. Присваивает результат в копию записи и возвращает её.

Три. call сам ничего не делает. Всю работу делает apply_fn(). В примере использования pipeline_each(), один экземпляр apply_fn() задаёт "country" значение "Canada". Другой – делает первую букву прописной.

Четыре. При выполнении экземпляра apply_fn() функции fn и key не будут доступны в области видимости. Это не аргументы apply_fn() и не локальные переменные. Но доступ к ним будет. При определении функции она сохраняет ссылки на переменные, которые она замыкает – те, что были определены снаружи функции, и используются внутри. При запуске функции переменные ищутся среди локальных, затем среди аргументов, а затем среди ссылок на замкнутые. Там и найдутся fn и key.

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

Молодцом. Замыкания, функции высшего порядка и область видимости – всё в нескольких параграфах. Можно и чайку с печеньками выпить.

Остаётся ещё одна обработка данных групп. Убрать всё, кроме имени и страны. Функция extract_name_and_country():

Def extract_name_and_country(band): plucked_band = {} plucked_band["name"] = band["name"] plucked_band["country"] = band["country"] return plucked_band print pipeline_each(bands, ) # => [{"name": "Sunset Rubdown", "country": "Canada"}, # {"name": "Women", "country": "Canada"}, # {"name": "A Silver Mt Zion", "country": "Canada"}]

Extract_name_and_country() можно было бы написать в обобщённом виде под названием pluck(). Использовалась бы она так:

Print pipeline_each(bands, )])

Упражнение 5 . pluck принимает список ключей, которые надо извлечь из записей. Попробуйте её написать. Это буде функция высшего порядка.

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

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

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

Сегодня познакомимся с простыми элементами, а сложные конструкции будут в других уроках.

Теория в теории

Как и в разговоре об ООП, так и о функциональном программировании, мы стараемся избегать определений. Все-таки четкое определение дать тяжело, поэтому здесь четкого определения не будет. Однако! Хотелки для функционального языка выделим:

  • Функции высшего порядка
  • Чистые функции
  • Иммутабельные данные

Это не полный список, но даже этого хватает чтобы сделать сделать "красиво". Если читателю хочется больше, то вот расширенный список:

  • Функции высшего порядка
  • Чистые функции
  • Иммутабельные данные
  • Замыкания
  • Ленивость
  • Хвостовая рекурсия
  • Алгебраические типы данных
  • Pattern matching

Постепенно рассмотрим все эти моменты и как использовать в Python.

А сегодня кратко, что есть что в первом списке.

Чистые функции

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

  • Легче читать и понимать код
  • Легче тестировать (не надо создавать «условий»)
  • Надежнее, потому что не зависят от «погоды» и состояния окружения, только от аргументов
  • Можно запускать параллельно, можно кешировать результат

Иммутабельные данные

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

Преимущества неизменяемых структур:

  • Безопасно разделять ссылку между потоками
  • Легко тестировать
  • Легко отследить жизненный цикл (соответствует data flow)

Функции высшего порядка

Функцию, принимающую другую функцию в качестве аргумента и/или возвращающую другую функцию, называют функцией высшего порядка :

Def f(x): return x + 3 def g(function, x): return function(x) * function(x) print(g(f, 7))

Рассмотрели теорию, начнем переходить к практике, от простого к сложному.

Списковые включения или генератор списка

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

Пример кода:

For x in xrange(5, 10): if x % 2 == 0: x =* 2 else: x += 1

Цикл с условием, подобные встречаются не редко. А теперь попробуем эти 5 строк превратить в одну:

>>>

Недурно, 5 строк или 1. Причем выразительность повысилась и такой код проще понимать - один комментарий можно на всякий случай добавить.

В общем виде эта конструкция такова:

Стоит понимать, что если код совсем не читаем, то лучше отказаться от такой конструкции.

Анонимные функции или lambda

Продолжаем сокращать количества кода.

Def calc(x, y): return x**2 + y**2

Функция короткая, а как минимум 2 строки потратили. Можно ли сократить такие маленькие функции? А может не оформлять в виде функций? Ведь, не всегда хочется плодить лишние функции в модуле. А если функция занимает одну строчку, то и подавно. Поэтому в языках программирования встречаются анонимные функции, которые не имеют названия.

Анонимные функции в Python реализуются с помощью лямбда-исчисления и выглядят как лямбда-выражения:

>>> lambda x, y: x**2 + y**2 at 0x7fb6e34ce5f0>

Для программиста это такие же функции и с ними можно также работать.

Чтобы обращаться к анонимным функциям несколько раз, присваиваем переменной и пользуемся на здоровье.

>>> (lambda x, y: x**2 + y**2)(1, 4) 17 >>> >>> func = lambda x, y: x**2 + y**2 >>> func(1, 4) 17

Лямбда-функции могут выступать в качестве аргумента. Даже для других лямбд:

Multiplier = lambda n: lambda k: n*k

Использование lambda

Функции без названия научились создавать, а где использовать сейчас узнаем. Стандартная библиотека предоставляет несколько функций, которые могут принимать в качестве аргумента функцию - map(), filter(), reduce(), apply().

map()

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

>>> list1 = >>> list2 = [-1, 1, -5, 4, 6] >>> list(map(lambda x, y: x*y, list1, list2)) [-7, 2, -15, 40, 72]

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

>>> [-7, 2, -15, 40, 72]

Итак, заметно, что использование списковых включений короче, но лямбды более гибкие. Пойдем дальше.

filter()

Функция filter() позволяет фильтровать значения последовательности. В результирующем списке только те значения, для которых значение функции для элемента истинно:

>>> numbers = >>> list(filter(lambda x: x < 5, numbers)) # В результат попадают только те элементы x, для которых x < 5 истинно

То же самое с помощью списковых выражений:

>>> numbers = >>>

reduce()

Для организации цепочечных вычислений в списке можно использовать функцию reduce(). Например, произведение элементов списка может быть вычислено так (Python 2):

>>> numbers = >>> reduce(lambda res, x: res*x, numbers, 1) 720

Вычисления происходят в следующем порядке:

((((1*2)*3)*4)*5)*6

Цепочка вызовов связывается с помощью промежуточного результата (res). Если список пустой, просто используется третий параметр (в случае произведения нуля множителей это 1):

>>> reduce(lambda res, x: res*x, , 1) 1

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

>>> reduce(lambda res, x: [x]+res, , )

Для наиболее распространенных операций в Python есть встроенные функции:

>>> numbers = >>> sum(numbers) 15 >>> list(reversed(numbers))

В Python 3 встроенной функции reduce() нет, но её можно найти в модуле functools.

apply()

Функция для применения другой функции к позиционным и именованным аргументам, заданным списком и словарем соответственно (Python 2):

>>> def f(x, y, z, a=None, b=None): ... print x, y, z, a, b ... >>> apply(f, , {"a": 4, "b": 5}) 1 2 3 4 5

В Python 3 вместо функции apply() следует использовать специальный синтаксис:

>>> def f(x, y, z, a=None, b=None): ... print(x, y, z, a, b) ... >>> f(*, **{"a": 4, "b": 5}) 1 2 3 4 5

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

Замыкания

Функции, определяемые внутри других функций, представляют собой замыкания. Зачем это нужно? Рассмотрим пример, который объяснит:

Код (вымышленный):

Def processing(element, type_filter, all_data_size): filters = Filter(all_data_size, type_filter).get_all() for filt in filters: element = filt.filter(element) def main(): data = DataStorage().get_all_data() for x in data: processing(x, "all", len(data))

Что можно в коде заметить: в этом коде переменные, которые живут по сути постоянно (т.е. одинаковые), но при этом мы загружаем или инициализируем по несколько раз. В итоге приходит понимание, что инициализация переменной занимает львиную долю времени в этом процессе, бывает что даже загрузка переменных в scope уменьшает производительность. Чтобы уменьшить накладные расходы использовать замыкания.

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

Научимся оформлять замыкания:

Def multiplier(n): "multiplier(n) возвращает функцию, умножающую на n" def mul(k): return n*k return mul # того же эффекта можно добиться выражением # multiplier = lambda n: lambda k: n*k mul2 = multiplier(2) # mul2 - функция, умножающая на 2, например, mul2(5) == 10

Заключение

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

3.2.3 Dictionary Comprehensions

Say we have a dictionary the keys of which are characters and the values of which map to the number of times that character appears in some text. The dictionary currently distinguishes between upper and lower case characters.

We require a dictionary in which the occurrences of upper and lower case characters are combined:

dct = { "a" : 10 , "b" : 34 , "A" : 7 , "Z" : 3 }

frequency = { k . lower () : dct . get (k . lower () , 0 ) + dct . get (k . upper () , 0 )

for k in dct . keys () }

print frequency # {"a": 17, "z": 3, "b": 34}

Python supports the creation of anonymous functions (i.e. functions that are not bound to a name) at runtime, using a construct called “lambda”. This is not exactly the same as lambda in functional programming languages, but it is a very powerful concept that’s well integrated into Python and is often used in conjunction with typical functional concepts like filter() , map() and reduce() .

Anonymous functions in the form of an expression can be created using the lambda
statement:

args is a comma-separated list of arguments, and expression is an expression involving those arguments. This piece of code shows the difference between a normal function definition and a lambda function:

def function (x ) :

return x * x

print function (2 ) # 4

#-----------------------#

function = lambda x : x * x

print function (2 ) # 4

As you can see, both function() do exactly the same and can be used in the same ways. Note that the lambda definition does not include a “return” statement - it always contains an expression which is returned. Also note that you can put a lambda definition anywhere a function is expected, and you don’t have to assign it to a variable at all.

The following code fragments demonstrate the use of lambda functions.

def increment (n ) :

return lambda x : x + n

print increment (2 ) # at 0x022B9530>

print increment (2 ) (20 ) # 22

The above code defines a function increment that creates an anonymous function on the fly and returns it. The returned function increments its argument by the value that was specified when it was created.

You can now create multiple different increment functions and assign them to variables, then use them independent from each other. As the last statement demonstrates, you don’t even have to assign the function anywhere - you can just use it instantly and forget it when it’s not needed anymore.

Q3. What is lambda good for?
Ans.
The answer is:

  • We don’t need lambda, we could get along all right without it. But…
  • there are certain situations where it is convenient - it makes writing code a bit easier, and the written code a bit cleaner.

Q4. What kind of situations?

Well, situations in which we need a simple one-off function: a function that is going to be used only once.

Normally, functions are created for one of two purposes: (a) to reduce code duplication, or (b) to modularize code.

  • If your application contains duplicate chunks of code in various places, then you can put one copy of that code into a function, give the function a name, and then - using that function name - call it from various places in your code.
  • If you have a chunk of code that performs one well-defined operation - but is really long and gnarly and interrupts the otherwise readable flow of your program - then you can pull that long gnarly code out and put it into a function all by itself.

But suppose you need to create a function that is going to be used only once - called from only one place in your application. Well, first of all, you don’t need to give the function a name. It can be “anonymous”. And you can just define it right in the place where you want to use it. That’s where lambda is useful.

Typically, lambda is used in the context of some other operation, such as sorting or a data reduction:

names = [ "David Beazley" , "Brian Jones" , "Raymond Hettinger" , "Ned Batchelder" ]

print sorted (names , key = lambda name : name . split () [ - 1 ] . lower () )

# ["Ned Batchelder", "David Beazley", "Raymond Hettinger", "Brian Jones"]

Although lambda allows you to define a simple function, its use is highly restricted. In
particular, only a single expression can be specified, the result of which is the return
value. This means that no other language features, including multiple statements, conditionals, iteration, and exception handling, can be included.
You can quite happily write a lot of Python code without ever using lambda. However,
you’ll occasionally encounter it in programs where someone is writing a lot of tiny
functions that evaluate various expressions, or in programs that require users to supply
callback functions.

You’ve defined an anonymous function using lambda, but you also need to capture the
values of certain variables at the time of definition.

>>> x = 10

>>> a = lambda y : x + y

>>> x = 20

>>> b = lambda y : x + y

Now ask yourself a question. What are the values of a(10) and b(10)? If you think the
results might be 20 and 30, you would be wrong:

The problem here is that the value of x used in the lambda expression is a free variable
that gets bound at runtime, not definition time. Thus, the value of x in the lambda
expressions is whatever the value of the x variable happens to be at the time of execution.
For example:

If you want an anonymous function to capture a value at the point of definition and
keep it, include the value as a default value, like this:

The problem addressed here is something that tends to come up in code that
tries to be just a little bit too clever with the use of lambda functions. For example,
creating a list of lambda expressions using a list comprehension or in a loop of some kind and expecting the lambda functions to remember the iteration variable at the time of definition. For example:

>>> funcs = [ lambda x : x + n for n in range (5 ) ]