Я собирался продолжить в том же духе и во второй части статьи представить свою библиотеку, добавляющую пару функциональных трюков в 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/
Добавить комментарий