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

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

    Подробнее
    Реклама
    Комментарии 15
    • 0
      А я перестал использовать функциональные возможности Python. Код с этими элементами иногда становится быстрее, причем значительно и считаю это плюсом. Но в замен получаю иногда сложные в чтении код, возвращаясь через неделю иногда по 5-10 мин. туплю над функцией :( «чем проще тем лучше», если что-то усложняет, то это не для Python. Может и ошибаюсь, это сугубо мой маленький опыт )
      • +4
        По моему опыту, функциональный стиль уменьшает сложность кода (если без фанатизма). Проблема может быть либо в том, что вы ещё не начали думать функционально или просто код недостаточно сложный, чтобы применять такой «язык более высокого уровня».
        • +1
          В питоне функциональное программирование, кроме тривиальных случаев, выглядит достаточно громоздко, да и лямбды в возможностях ограничены… Но в случаях, когда оно применимо, действительно понимание действия кода при чтении обычно лучше (конечно, при условии что читающий знаком с парадигмой).
      • +7
        Очень не хватает Pattern Matching. Без него многие вещи получаются неочевидными с первого взгляда.

        А потом я начинаю программировать на VBA и понимаю, что не всё так плохо.
      • 0
        Хорошо. Местами очень.
        • 0
          Библиотека интересная. Но больше похоже, что это просто набор удобных небольших функций (ну вроде re_find), которые к ФП относятся постольку-поскольку. Вот, скажем, функции join и merge. Они ведь модифицирует свой первый аргумент. Тогда в чем их функциональность?
          • +1
            join() и merge() не меняют свои аргументы, также как остальные функции в funcy
          • +1
            Проблема таких библиотек как funcy в том, что каждый горазд написать свою собственную. Наверное, многие обнаружили в примерах функции, которые не раз писали сами. По-хорошему, большинство этих функций должны быть в стандартной библиотеке, раскиданные по модулям functools, itertools и т. д.
            • +7
              Кстати, обращаюсь к Suor и всем: пожалуйста, называйте свои посты как можно более информативно. Экономьте время окружающих на понимание сути поста. Например, я бы назвал эту статью «Дополнительные функциональные инструменты для Python».
              • +4
                Хотите еще больше функциональщины? Попробуйте свою библиотеку вместе с вот этой github.com/kachayev/fn.py. Она добавит вам обобщение итераторов и генераторов без использования chain, новый выразительные lambda, 20+ новых рецептов для itertools не говоря уже о возможности удобно сделать tail call optimization.
                • 0
                  Может, вы знаете готовую отлаженную библиотеку, позволяющую вместо
                  list(take(10, fib))
                  писать
                  fib.take(10).as_list()
                  ?
                  • +1
                    take() возвращает список, так что list() не нужен, а второй вариант слишком не по-питоньи, я бы не стал так писать, чтобы не путать людей. Тут лучше в сторону ruby смотреть
                • 0
                  ОМГ! Это именно то чего мне не хватало, список публичных методов и склеивание словарей.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.