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

в 17:28, , рубрики: functional programming, languages, python, функциональное программирование, метки: , ,

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js