Вот уже несколько лет функциональное программирование набирает популярность. Это, конечно, не значит, что люди забрасывают свои старые языки и ООП и массово переходят на 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. Итак, всем кто хочет пополнить свой словарный запас — добро пожаловать.
Автор: Suor