Не ещё одна статья о функциональном программировании

от автора

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

Я собирался продолжить в том же духе и во второй части статьи представить свою библиотеку, добавляющую пару функциональных трюков в python, но потом понял, что фокус моей библиотеки не на функциональном программировании, а на практичности. На этом я и сосредоточюсь, приведу несколько жизненных примеров полезности funcy.

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

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

Несложные манипуляции с данными

1. Объединить список списков. Традиционно я делал это таким образом:

from operator import concat reduce(concat, list_of_lists)  # Или таким: sum(list_of_lists, [])  # Или таким: from itertools import chain list(chain.from_iterable(list_of_lists)) 

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

from funcy import cat cat(list_of_lists) 

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

from funcy import mapcat mapcat(str.splitlines, bunch_of_texts) 

разберёт все строки в текстах в один плоский список. Для обеих функций есть ленивые версии: icat() и imapcat().

2. Сложить несколько словарей. В питоне есть несколько неуклюжих способов объединять словари:

d1.update(d2)  # Изменяет d1 dict(d1, **d2) # Неудобно для > 2 словарей  d = d1.copy() d.update(d2) 

Я всегда удивлялся почему их нельзя просто сложить? Но имеем то, что имеем. В любом случае, с funcy это делается легко:

from funcy import merge, join merge(d1, d2) merge(d1, d2, d3) join(sequence_of_dicts) 

Но merge() и join() могут объединять не только словари, они работают практически для любых коллекций: словарей, упорядоченных словарей, множеств, списков, кортежей, итераторов и даже строк.

3. Захват подстроки с помощью регулярного выражения. Обычно это делается так:

m = re.search(some_re, s) if m:     actual_match = m.group() # или m.group(i), или m.groups()     ... 

С funcy это превращается в:

from funcy import re_find actual_match = re_find(some_re, s) 

Если это не кажется вам достаточно впечатляющим, то взгляните на это:

from funcy import re_finder, re_all, partial, mapcat  # Вычленяем числа из каждого слова map(re_finder('\d+'), words)  # Парсим ini файл (re_finder() возвращает кортежи когда в выражении > 1 захвата) dict(imap(re_finder('(\w+)=(\w+)'), ini.splitlines()))  # Вычленяем числа из строк (возможно по нескольку из каждой) и объединяем в плоский список mapcat(partial(re_all, r'\d+'), bunch_of_strings) 

Отступление про импорты и практичность

Как вы могли заметить, я импортирую функции напрямую из funcy, не используя какие-либо подпакеты. Причина, по которой я остановился на таком интерфейсе, — практичность; было бы довольно занудным требовать от всех пользователей моей библиотеки помнить откуда нужно импортировать walk() из funcy.colls или funcy.seqs, кроме того, многострочные импорты в начале каждого файла и без меня есть кому набивать.

Дополнительным преимуществом такого решения является возможность просто написать:

from funcy import * 

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

Кое-какие более функциональные штучки

Мы уже видели пару примеров использования функций высшего порядка — re_finder() и partial(). Стоит добавить, что сама функция re_finder() является частичным применением re_find() созданным для удобства применения в map() и ей подобных. И естественным образом, с filter() удобно использовать re_tester():

# Выбираем все приватные атрибуты объекта is_private = re_tester('^_') filter(is_private, dir(some_obj)) 

Отлично, мы можем задать несколько предикатов, таких как is_private(), и фильтровать атрибуты объекта по ним:

is_special = re_tester('^__.+__$') is_const = re_tester('^[A-Z_]+$') filter(...) 

Но, что если мы хотим получить список публичных атрибутов или приватных констант, что-то задействующее комбинацию предикатов? Легко:

is_public = complement(is_private) is_private_const = all_fn(is_private, is_const) either_const_or_public = any_fn(is_const, is_public) 

Для удобства также есть функция, дополняющая filter():

remove(is_private, ...) # то же, что filter(is_public) 

Надеюсь все утолили свой функциональный аппетит, потому пора перейти к чему-нибудь менее абстрактному.

Работа с коллекциями

Кроме утилит для работы с последовательностями, коих много больше, чем я тут описал, funcy также помогает работать с коллекциями. Основу составляют функции walk() и select(), которые аналогичны map() и filter(), но сохраняют тип обрабатываемой коллекции:

walk(inc, {1, 2, 3}) # -> {2, 3, 4} walk(inc, (1, 2, 3)) # -> (2, 3, 4)  # при обработке словаря мы работаем с парами ключ-значение swap = lambda (k, v): (v, k) walk(swap, {1: 10, 2: 20}) # -> {10: 1, 20: 2}  select(even, {1, 2, 3, 10, 20}) # -> {2, 10, 20}  select(lambda (k, v): k == v, {1: 1, 2: 3}) # -> {1: 1} 

Эта пара функций подкрепляется набором для работы со словарями: walk_keys(), walk_values(), select_keys(), select_values():

# выберем публичную часть словаря атрибутов объекта select_keys(is_public, instance.__dict__)  # выбросим ложные значения из словаря select_values(bool, some_dict) 

Последний пример из этой серии будет использовать сразу несколько новых функций: silent() — глушит все исключения, бросаемые оборачиваемой функцией, возвращая None; compact() — убирает из коллекции значения None; walk_values() — обходит значения переданного словаря, конструируя новый словарь с значениями, преобразованными переданной функцией. В целом эта строка выбирает словарь целочисленных параметров из параметров запроса:

compact(walk_values(silent(int), request_dict)) 

Манипулирование данными

О! Мы добрались до самого интересного, сюда часть примеров я включил просто потому, что они кажутся мне клёвыми. Хотя, если честно, я делал это и выше. Сейчас мы будем разделять и группировать:

# отделим абсолютные URL от относительных absolute, relative = split(re_tester(r'^http://'), urls)  # группируем посты по категории group_by(lambda post: post.category, posts) 

Собирать плоские данные во вложенные структуры:

# строим словарь из плоского списка пар dict(partition(2, flat_list_of_pairs))  # строим структуру учётных данных {id: (name, password) for id, name, password in partition(3, users)}  # проверяем, что список версий последователен assert all(prev + 1 == next for prev, next in partition(2, 1, versions)):  # обрабатываем данные кусками for chunk in chunks(CHUNK_SIZE, lots_of_data):     process(chunk) 

И ещё пара примеров, просто до кучи:

# выделяем абзацы красной строкой for line, prev in with_prev(text.splitlines()):     if not prev:         print '    ',     print line  # выбираем пьесы Шекспира за 1611 год where(plays, author="Shakespeare", year=1611) # => [{"title": "Cymbeline", "author": "Shakespeare", "year": 1611}, #     {"title": "The Tempest", "author": "Shakespeare", "year": 1611}] 

Не просто библиотека

Возможно, некоторые из вас встретили знакомые функции из Clojure и Underscore.js (кстати, пример с Шекспиром нагло содран из документации последней), — ничего удивительного, я во многом черпал вдохновение из этих источников. При этом я старался следовать питоньему стилю, сохранять консистентность библиотеки и нигде не жертвовать практичностью, поэтому не все функции полностью соответствуют своим прототипам, они скорее соответствуют друг другу и стандартной библиотеке.

И ещё одна мысль. Мы привыкли называть языки программирования языками, при этом редко осознаём, что синтаксические конструкции и стандартные функции — это слова этих языков. Мы можем добавлять свои слова, определяя функции, но обычно такие слова слишком специфичны, чтобы попасть в повседневный языковой словарь. Утилиты из funcy, напротив, заточены под широкую область применения, поэтому эту библиотеку можно воспринимать как расширение python, также как underscore или jQuery — расширение JavaScript. Итак, всем кто хочет пополнить свой словарный запас — добро пожаловать.

ссылка на оригинал статьи http://habrahabr.ru/post/174619/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *