functools (это такая свалка для всяких ненужных мне вещей :-).
— Гвидо ван Россум

Может показаться, что статья о ФП, но я не собираюсь обсуждать парадигму. Речь пойдет о переиспользовании и упрощении кода — я попытаюсь доказать, что вы пишете слишком много кода, поэтому он сложный и тяжело тестируется, но самое главное: его долго читать и менять.


В статье заимствуются примеры и/или концепции из библиотеки funcy. Во-первых, она клевая, во-вторых, вы сразу же сможете начать ее использовать. И да, нам понадобится ФП.


Кратко о ФП


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

ФП также присущи следующие приемы:


  • частичное применение
  • композирование (в python еще есть декораторы)
  • ленивые вычисления

Если вам все это уже знакомо, переходите сразу к примерам.


Чистые функции


Чистые функции зависят только от своих параметров и возвращают только свой результат. Следующая функция вызванная несколько раз с одним и тем же аргументом выдаст разный результат (хоть и один и тот же объект, в данном случае %).


Напишем функцию-фильтр, которая возвращает список элементов с тру-значениями.


pred = bool
result = []

def filter_bool(seq):
    for x in seq:
        if pred(x):
            result.append(x)
    return result

Сделаем ее чистой:


pred = bool

def filter_bool(seq):
    result = []
    for x in seq:
        if pred(x):
            result.append(x)
    return result

Теперь можно вызвать ее лярд раз подряд и результат будет тот же.


Функции высшего порядка


Это такие функции, которые принимают в качестве аргументов другие функции или возвращают другую функцию в качестве результата.


def my_filter(pred, seq):
    result = []
    for x in seq:
        if pred(x):
            result.append(x)
    return result

Мне пришлось переименовать функцию, потому что она теперь куда полезнее:


above_zero = my_filter(bool, seq)
only_odd = my_filter(is_odd, seq)
only_even = my_filter(is_even, seq)

Заметьте, одна функция и делает уже много чего. Вообще-то, она должна быть ленивой, делаем:


def my_filter(pred, seq):
    for x in seq:
        if pred(x):
            yield x

Вы заметили, что мы удалили код, а стало только лучше? Это лишь начало, скоро мы будем писать функции только по праздникам. Вот смотрите:


my_filter = filter

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


Частичное применение


Это процесс фиксации части аргументов функции, который создает другую функцию, меньшей арности. В переводе на наш это functools.partial.


filter_bool = partial(filter, bool)
filter_odd = partial(filter, is_odd)
filter_even = partial(filter, is_even)

Я понимаю, что это все азы ФП, но хочу отметить, что мы не написали ничего нового: мы взяли уже готовые функции и сделали другие. Основа новых — очень маленькие, простые, легкотестируемые функции, мы можем без опаски использовать их для создания более сложных.


Композирование


Такой простой, крутой и нужной штуки в python нет. Ее можно написать самостоятельно, но хотелось бы вменяемой сишной имплементации :(


def compose(*fns):
    init, *rest = reversed(fns)
    return lambda *a, **kw: reduce(lambda a, b: b(a), rest, init(*a, **kw))

Теперь мы можем делать всякие штуки (выполнение идет справа налево):


mapv = compose(list, map)
filterv = compose(list, filter)

Это прежние версии map и filter из второй версии python. Теперь, если вам понадобится неленивый map, вы можете вызвать mapv. Или по старинке писать чуть больше кода. Каждый раз.


Функции compose и partial прекрасны тем, что позволяют переиспользовать уже готовые, оттестированные функции. Но самое главное, если вы понимаете преимущество данного подхода, то со временем станете сразу писать их готовыми к композиции.


Это очень важный момент — функция должна решать одну простую задачу, тогда:


  • она будет маленькой
  • ее будет проще тестировать
  • легко композировать
  • просто читать и менять
  • тяжело сломать

Пример


Задача: дропнуть None из последовательности.
Решение по старинке (чаще всего даже не пишется в виде функции):


no_none = (x for x in seq if x is not None)

Обратите внимание: без разницы как называется переменная в выражении. Это настолько неважно, что большинство программистов тупо пишут x, чтобы не заморачиваться. Все пишут этот бессмысленный код раз за разом. Каждый цензура раз: for, in, if и несколько раз x — потому что для компрехеншена нужен scope и у него есть свой синтаксис. Мы пишем: на каждую итерацию цикла присвоить переменной значение. И оно присваивается, и проверяется условие.


Мы каждый раз пишем этот бойлерплейт и пишем тесты на этот бойлерплейт. Зачем?


Давайте перепишем:


from operator import is_
from itertools import filterfalse
from functools import partial

is_none = partial(is_, None)
filter_none = partial(filterfalse, is_none) 

# Использование
no_none = filter_none(seq)

# Переиспользование
all_none = compose(all, partial(map, is_none))

Все. Никакого лишнего кода. Мне приятно такое читать, потому что этот код (no_none = filter_none(seq)) очень простой. То, как работает это функция, мне нужно прочитать ровно один раз за все время в проекте. Компрехеншен вам придется читать каждый раз, чтобы точно понять что оно делает. Ну или засуньте ее в функцию, без разницы, но не забудьте про тесты.


Пример 2


Довольно частая задача получить значения по ключу из массива словарей.


names = (x['name'] for x in users)

Кстати, работает очень быстро, но мы снова написали кучу ненужной фигни. Перепишем, чтобы работало еще быстрее:


from operator import itemgetter

def pluck(key, seq):
    return map(itemgetter(key), seq)

# Использование
names = pluck('name', users)

А как часто мы это будем делать?


get_names = partial(pluck, 'name')
get_ages = partial(pluck, 'age')

# Сложнее
get_names_ages = partial(pluck, ('name', 'age'))
users_by_age = compose(dict, get_names_ages)

ages = users_by_ages(users)  # {x['name']: x['age'] for x in users}

А если у нас объекты? Пф, параметризируй это:


from operator import itemgetter, attrgetter

def plucker(getter, key, seq):
    return map(getter(key), seq)

pluck = partial(plucker, itemgetter)
apluck = partial(plucker, attrgetter)

# Использование
names = pluck('name', users)  # (x['name'] for x in users)
object_names = apluck('name', users)  # (x.name for x in users)

# Геттеры умеют сразу таплы данных
object_data = apluck(('name', 'age', 'gender'), users)  # ((x.name, x.age, x.gender) for x in users)

Пример 3


Представим себе простой генератор:


def dumb_gen(seq):
    result = []
    for x in seq:
        # здесь что-то проиcходит
        result.append(x)
    return result

Тут полно бойлерплейта: мы создаем пустой список, затем пишем цикл, добавляем элемент в список, отдаем его. Кажется, я буквально перечислил все тело функции :(


Правильным решением будут использование filter(pred, seq) или map(func, seq), но иногда нужно сделать что-то сложнее, т.е. генератор написать действительно нужно. А если результат всегда нужен в виде списка или тапла? Да легко:


@post_processing(list)
def dumb_gen(seq):
    for x in seq:
        ...
        yield x

Это параметрический декоратор, работает он так:


result = post_processing(list)(dumb_gen)(seq)

Т.е. результатом первого вызова будет новая функция, которая примет функцию в качестве аргумента и вернет другую функцию. Звучит сложнее, чем есть:


def post_processing(post):
    return lambda func: compose(post, func)

Обратите внимание, я использовал уже существующую compose. Результат — новая функция, которую никто не писал.
А теперь стихи:


post_list = post_processing(list)
post_tuple = post_processing(tuple)
post_set = post_processing(set)
post_dict = post_processing(dict)
join_comma = post_processing(', '.join)

@post_list
def dumb_gen(pred, seq):
    for x in seq:
        ...
        yield x

Куча новых функций по цене одной! И я убрал бойлерплейт, функция стала меньше и намного симпатичнее.


Итог


Перебирая данные железобетонными функциями (чистыми, высшими), мы сохраняем простоту реализации и обеспечиваем стабильность программы, которую проще тестировать:


  • пишите чистые функции, они обеспечат стабильность программы
  • пишите функции высшего порядка, код станет намного компактнее и надежнее
  • композируйте, декорируйте, частично применяйте, переиспользуйте код
  • используйте сишные либы, они дадут скорости вашему софту

Как только вы напишете свой набор инструментов, новый код будет создаваться со знанием того, что у вас есть штука, которая может решить часть задачи. А значит софт будет меньше и проще.


С чего начать?


  • обязательно ознакомьтесь с itertools, functools, operator, collections, в особенности с примерами в конце
  • загляните в документацию funcy или другой фпшной либы, почитайте исходный код
  • напишите свой funcy, весь он сразу вам не нужен, но опыт очень пригодится

Credits


В моем случае, использование ФП началось со знакомства с clojure — это штука капитально выворачивает мозги, настоятельно рекомендую посмотреть хотя бы видосы на ютубе.


Clojure как-то так устроен, что вам приходится писать проще, без привычных нам вещей: без переменных, без любимого стиля "романа", где сначала мы раскрываем личность героя, потом пускаемся в его сердечные проблемы. В clojure вам приходится думать %) В нем только базовые типы данных и "отсутствие синтаксиса" (с). И эту "простую" концепцию, оказывается, можно портировать в python.


UPD


Похоже, у читателей сложилось впечатление, будто я пишу сплошным ФП. Хочу всех успокоить: функциональный подход я использую исключительно в местах, где пишется код, который я уже писал. На мой взгляд, повторять "рабочие" приемы всякий раз глупо и бессмысленно, поэтому перевожу подобные куски в функции и использую их повторно. Рабочий пример можно посмотреть в комментарии.

Комментарии (88)


  1. datacompboy
    09.01.2018 22:08

    Вот так смотришь на синтетику и непонятно «а нафига они нам нужны» (ц) Слоник.
    Как с другими задачами — уж больно вы далеки от народа.

    Рассказывая о простоте и упрощении, следует подходить с практической позиции, когда для реализации высокоуровневой логики от этого есть прок.

    А то вместо ванлайнера (или двулайнера) показывают модуль в десяток строк… проще? r'ly?

    И да, композиции тоже надо тестировать. Ни smallcheck ни quickcheck как-то не завезли же.


    1. magic4x Автор
      09.01.2018 22:39

      Если написать функцию для удаления нанов и затем ее протестировать, не придется тестировать ее поведение в каждом частном куске кода. Код будет меньше, код будет проще, код будет стабильнее. Функции в отдельности, композированные тоже, конечно нужно тестировать.


      Про "далеки от народа" не понял. Хотя могу предположить, что я предлагаю несколько непривычный для python подход — все в порядке, я уже делаю это не в первый раз. Сначала никому не нравится, потом за уши не оторвать. Уж очень красочно выглядит экран функций из одних compose.


      Кстати, это не синтетика, я этим реально пользуюсь в работе. На том же pluck (более сложном, конечно же) у меня построен мини DSL для работы со списками и словарями — выкинули кучу кода.


      1. random1st
        10.01.2018 00:34
        +1

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


        1. ser0t0nin
          10.01.2018 14:41

          ох не зарекайся насчет пепла и канав


      1. daiver19
        10.01.2018 02:25

        Так пишите функцию, кто вам запрещает? Непонятно только зачем все эти функциональные навороты, когда проблема аналогично решается выносом императивно «бойлерплэйта» в отдельную функцию. А еще «pluck» — ужасное имя для функции, лучше бы над именованием задуматься, чем над тем, как извратиться в написании тривиального кода.


        1. deksden
          10.01.2018 17:20

          В компьютерных науках есть только две сложные вещи: инвалидация кэша и именование всяких штук )) Второе!


        1. MikailBag
          10.01.2018 17:57

          Ну в lodashjs тоже pluck.


      1. werevolff
        10.01.2018 18:35

        Немного не понял относительно написания своей функции, которую мы протестируем один раз. Что это за юзкейс? Я пишу утилиту для дропа None из массива. Что я делаю дальше? Пакую её в пакет, выкладывают в сеть и жду, когда её начнут использовать все повсеместно? Скорее-всего, эта функция будет использоваться внутри большого приложения, выполняя простейшую операцию. Но, если часть используемых утилит или встроенных функций, которые задействованы в моих кастомных тулзах поменяют своё поведение после апдейта, мне потребуется всё заново отдебажить, переписать код и тесты. Единственное преимущество такого подхода — я получу более быструю утилиту. Однако, ситуации, когда от её скорости есть толк, будут встречаться пару раз в коде. С другой стороны, если этот код попадёт на обслуживание другому программеру, он будет плеваться. Действительно, какой смысл во всех этих обёртках? Доказать, что автор это может сделать? Что он офигенно крут?


    1. iroln
      10.01.2018 03:02

      Ни smallcheck ни quickcheck как-то не завезли же.

      Если я правильно понял вашу мысль, то может вот это подойдёт: http://hypothesis.works/ ?


  1. mistiman
    09.01.2018 22:39

    Боюсь за такие "упрощения" коллеги меня будут бить. Код становится не читаемым без видимых на то причин. Проще лучше


    1. magic4x Автор
      09.01.2018 22:46

      Дело привычки, поверьте.


      1. mistiman
        10.01.2018 00:20

        Боюсь я был не понят) Люблю фп, но там где он реально уместен. «Упрощать» сomprehensions через создание функции это что-то лишнее.
        сomprehensions прочитает любой знакомый с питоном, а чтобы развернуть в голове вот эти функции нужно либо знать все использованные функции высшего порядка, либо все их просмотреть, а значит неоднократно переместиться по коду проекта.
        Лучше пусть будет на 2,3,5 строк больше, но чтобы это читалось проще.


        1. magic4x Автор
          10.01.2018 13:27

          Делайте проще:


          from mytools import mytool
          
          myresult = mytool(mydata)

          Импорт-то прочитать не сложно. Ну и по опыту: в большом проекте это "2-5" строк кода множатся как грибы после дождя, причем бессмысленно.


          1. Afti
            10.01.2018 14:06
            +1

            И все. Никакого лишнего кода.)


    1. pfemidi
      09.01.2018 23:13

      Для функциональщиков это может быть и проще, но для императивщиков… Я лично глаза мозги сломал пока разложил всё это мысленно в обычные императивные функции, понял как это работает и понял что лично я так никогда делать не буду :-)


      1. lxsmkv
        10.01.2018 00:50

        Согласен. Сам стараюсь писать так, чтобы те кто с языком не знакомы могли понимать как код работает. В ущерб самолюбию. Это моя интерпретация догмы «Explicit is better than implicit» — сторониться неочевидного.

        «Если долго вглядываться в код, увидишь, что это всего лишь набор символов.» © Я.


  1. 0x18h
    09.01.2018 23:07

    Теперь можно вызвать ее лярд раз под ряд и результат будет тот же.

    А разве это чистая функция раз её результат зависит от глобальной переменной pred?


    1. magic4x Автор
      09.01.2018 23:17

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


      1. domix32
        10.01.2018 10:47
        +1

        Lasciate ogni speranza, voi ch’entrate


    1. grieverrr
      10.01.2018 17:51

      вот вот, дальше не читал.


  1. lgorSL
    10.01.2018 00:31

    Мне кажется, проблема синтаксиса Питона в том, что код типа такого:


    no_none = (x for x in seq if x is not None)

    пишется короче, чем в функциональном стиле


    no_none = filter(lambda x: x is not None, seq)

    Всё равно придётся написать 'x' целых два раза и ещё слово lambda появится. Если захотеть, чтобы no_none была списком, а не генератором, то станет ещё хуже:


    no_none = [x for x in seq if x is not None]

    vs


    no_none = list(filter(lambda x: x is not None, seq))

    Появилось ещё одно слово и вложенные скобочки. Нельзя просто так взять и написать на питоне красиво и функционально — чтобы получить какой-то выигрыш в краткости, надо брать что-то реально повторяющееся.
    В некоторых языках происходит наоборот — они подталкивают к функциональному стилю как более простому и короткому, например:


    val no_none = seq filter (_ != null)

    Я время от времени порываюсь написать что-то функциональное на питоне, но почти всё время остаётся чувство, что проще и короче написать решение "в лоб".


    1. DaneSoul
      10.01.2018 12:04

      Почему Вы противопоставляете comprehensions и функциональный стиль?

      Оно даже в официальном мануале идет в разделе функционального прогаммирования: docs.python.org/3/howto/functional.html

      List comprehensions and generator expressions are a concise notation for such operations, borrowed from the functional programming language Haskell


      1. potan
        10.01.2018 21:52

        В питоне comprehensions реализован не совсем функционально — он образается с переменной как с мутабельной:

        Python 3.3.5 (default, Dec 11 2015, 11:33:43) [MSC v.1800 32 bit (Intel)] on win32
        Type "help", "copyright", "credits" or "license" for more information.
        >>> x = [lambda x: x+i for i in [1,2,3,4]]
        >>> x[0](1)
        5

        А во python2 даже портит ее в скопе.


    1. abogoyavlensky
      10.01.2018 13:19

      Можно даже чуть короче: filter(None, seq)


      1. anti1869
        10.01.2018 14:49

        Это отфильтрует в том числе 0, False, "", etc.


  1. evgenyk
    10.01.2018 01:04

    Для меня идеальный код, это когда его читаешь как текст.
    И кстати, зачем в первом примере pred = bool глобальная переменная?


    1. trapwalker
      10.01.2018 14:34

      Полагаю автор хотел довести нас к прозрению малыми шагами, а вышло как всегда: кто и без того понимал суть — стали придираться; кому оно не сильно надо — всё равно споткнулись и оставили; наверно, единицы кому зашло с благодарностью улыбнулись и пошли дальше.
      Автор молодец, он придумал хорошие примеры, обосновал их полезность, разжевал и отполировал изложение. Кому-то будет полезно наверняка. Я лично почерпнул лишь некоторые тонкости манеры изложения, и оно того стоило, знаете ли. Какой-нибудь пример обязательно пригодится, когда в очередной раз придётся рассказывать молодым коллегам основы.


  1. Lure_of_Chaos
    10.01.2018 01:06

    Извините, но не нужно из python делать clojure (извините, lisp). Да, чистые функции предсказуемы, тестируемы, хорошо параллелятся. Да, функции высшего порядка более универсальны. Да, map reduce легче читается, чем for if for for.
    Но все же надо знать меру, и не нужно вводить кучу новых функций ради композиции и каррирования, не нужно более читаемые for if for for заменять совершенно нечитаемыми вложенными скобками, не нужно в худших традициях ФП вместо трансформации списков заниматься их копированием.
    Возьмите лучшее из обоих миров и спокойно пишите понятный код, не наживая себе хаскель головного мозга, не беспокоясь о том, что где-то не слишком Функционально.
    Ну а за попытку из изначально императивного (знаю, мультипарадигменного, но все же) языка сделать чисто функциональный — статье плюс.


    1. trapwalker
      10.01.2018 14:47

      Ой, не сгущайте краски, коллега. Вы, конечно правы, но никто никого, как мне кажется, не пытается заставить превращать один язык в другой. Понимать концепцию всегда полезно и для новичков, например, через питон это сделать. может быть будет иногда лучше.
      Я тоже с трудом вижу где бы такой перефункционализированный подход улучшил питоновский код в обыденной жизни. Однако, как говорится, хорошо подобранным примером можно доказать всё что угодно.
      Давайте напряжемся и придумаем за автора (как адвокаты дьявола) пример в его пользу. Это же интересно.
      Мне приходит на ум что-то вроде задач сложной настраиваемой обработки потоков данных, когда набор преобразований, применяемых к потоку, требуется сделать кастомизируемым, прозрачным и поддающимся контролю. Я про те самые случаи. когда ООП с его состояниями побочными эффектами плавно превращается в геморрой из фабрик, куч, пуллов, очередей и прочего. Не знаю даже. Подумаю еще=).


      1. magic4x Автор
        10.01.2018 17:15

        Отвечу сразу на два ваших комментария: во-первых, спасибо за лестный отзыв выше, очень непросто написать статью и учесть знания/опыт всей аудитории, и уместить это в размере поста; во-вторых, реальный пример:


        @post_mapping(foo)
        def bar(self, data):
            yield from cat(keep('key', data.values()))
            yield self.baz 

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


        И так:


        • post_mapping вызовет foo на каждый элемент отданным генератором, аналог map(foo, bar()). Только не придется писать это всякий раз
        • cat — это шорткат к itertools.chain.from_iterable — склеивает массивы вместе в один
        • keep — это комбинация "достань по ключу key и дропни фолс-значения"

        По порядку:


        • из значений словаря по ключу key достаются значения (в данном случае это массивы), затем удаляются пустые, затем объединяются в один и отдаются
        • в хвост генератора добавляется self.baz
        • все элементы обрабатываются функцией foo

        Императивно это выглядит раза в 4 больше. И дело тут не в размере, а в том, что используя одни и те же инструменты (и понимая как они работают), вам не нужно читать и писать код, который делает то же самое, но конкретно тут и конкретно так каждый раз.


        И оно ленивое!


  1. aavezel
    10.01.2018 09:10
    +10

    На улице 2018… А всё как 8 лет назад — по основам функционального кода в питоне пишут статьи, а в комментах идет срач, о том что императивный стиль понятнее…


  1. domix32
    10.01.2018 10:46
    -1

    Не понял зачем писать собственные варианты фильтра, когда в области видимости без импортов уже есть `map`, `filter` и `reduce`.


    1. brake
      10.01.2018 11:59
      -1

      Вероятно для того, чтобы создать самодокументированный код (к чему очень стимулирует, хотя бы, clojure). Типа вместо filter(много параметров), который затрудняет чтение кода, мы, сначала определяем filter_none() и потом его используем как предикат. Читающему глазу уже становится легче.


      1. magic4x Автор
        10.01.2018 13:22
        -1

        Все проще: map, filter и reduce питонисты не используют. Я серьезно.


        1. KMiNT21
          10.01.2018 13:40
          +2

          Ну зачем же за всех говорить. :) Вот лично я filter всегда использую, а map — из модуля multiprocessing в виде multiprocessing.Pool().map

          Плюс записи вроде «sume_func = lambda x: .....xyz» внутри функций, чтобы ничего лишнего оттуда не выносить «наверх».


        1. domix32
          10.01.2018 14:13

          Причины не использовать их?


          1. myrslok
            10.01.2018 15:42

            Comprehensions идиоматичнее.


            1. evocatus
              11.01.2018 11:12

              Спорно. К тому же если использовать модуль operator, то map, filter и reduce становятся короче и легче читаются, чем лямбды (у них в Python настолько уродский синтаксис — если хотите поспорить, то посмотрите на синтаксис лямбд в том же Clojure, не говоря уже про haskell, где даже обычные функции объявляются лаконичнее, чем лямбды в Python).
              P.S. Напоминаю, что list comprehensions и генераторы скопированы в Python из Haskell


              1. myrslok
                11.01.2018 15:11
                +1

                Что-то я не улавливаю, при чем тут лямбды, и совсем не улавливаю, при чем тут Haskell.


                Имеется в виду что-то такое:


                [x + 1 for x in numbers if x % 2 == 0]

                Я утверждаю, что в Python принято писать так, а не с filter и map (как, кстати, это записать короче с помощью operator?).


                1. evocatus
                  11.01.2018 22:24

                  Лучше использовать по возможности круглые скобочки, чтобы получить generator expression, потому что ваш list comprehension с range(1000000) на Python 3.6 64-bit на Core i5-6500 у меня завис намертво.

                  from operator import *
                  from functools import partial
                  map(partial(add, 1), filter(lambda x: x%2==0, numbers))


                  Или так:
                  from operator import *
                  from functools import partial
                  
                  def inc(x):
                      return x+1
                  
                  def divisible_by(x):
                      return lambda y: mod(x, y) == 0
                  
                  map(inc, filter(divisible_by(2), numbers))


                  Я мог бы сказать, что все эти inc и divisible_by объявляются один раз и выделяются в модуль myshinyfp.py, а потом переиспользуются. Но я даже не буду настаивать на своём, если вы скажете, что это длинно. Но это гибче, изящнее, правильнее.


                  1. myrslok
                    12.01.2018 12:15
                    +1

                    Не могу согласиться, что круглые скобочки "лучше", они просто имеют другую семантику. Эти вопросы ортогональны обсуждаемым.


                    Что касается остального, у вас получилось намного длиннее, так что настаивать на обратном было бы смело. Как я считаю, у вас значительно менее Pythonic. И лямбды как раз у вас, а не у меня. Но уж лучше лямбда, чем partial(add, 1) (кстати, было бы интересно попрофилировать, подозреваю это медленнее и лямбды, и comprehension). И совсем нет никаких причин для mod(x, y) вместо x % y.


                    1. evocatus
                      12.01.2018 18:20

                      Я прекрасно понимаю разницу между списком и генератором. Просто имел в виду, что если можно использовать генератор, то лучше использовать генератор, потому что его создание «дешевле».


                  1. anjensan
                    12.01.2018 13:20

                    На самом деле не обязательно.

                    Python 3.6.4 (default, Dec 21 2017, 01:35:12) 
                    [GCC 4.9.2] on linux
                    Type "help", "copyright", "credits" or "license" for more information.
                    >>> from timeit import timeit
                    >>> ns = list(range(1000000))
                    >>> timeit(lambda: sum((x + 1 for x in ns if x % 2 == 0)), number=10)
                    0.7995037079999747
                    >>> timeit(lambda: sum([x + 1 for x in ns if x % 2 == 0]), number=10)
                    0.7783708530000695
                    

                    Ну раз уж говорим про скорость, варианты с map/filter будут еще медленнее.
                    >>> from operator import *
                    >>> from functools import partial
                    >>> timeit(lambda: sum(map(partial(add, 1), filter(lambda x: x % 2 == 0, ns))), number=10)
                    1.7735814190000383
                    


                    1. myrslok
                      12.01.2018 13:43

                      Так я и писал, что с partial(add, 1) будет медленнее. Запустите, если не затруднит, еще и так:


                      timeit(lambda: sum(map(lambda x: x + 1, filter(lambda x: x % 2 == 0, ns))), number=10)


                      1. anjensan
                        12.01.2018 13:56

                        Я отвечал evocatus. Но вообще partial быстрее чем аналогичная лямбда.

                        >>> timeit(lambda: sum(map(lambda x: x + 1, filter(lambda x: x % 2 == 0, ns))), number=10)
                        1.7567518400001063
                        Тут вариант без partial быстрее за счет того, что + (один опкод) быстрее чем add (полноценная функция, хоть и написанная на C).
                        >>> timeit(lambda: sum(map(lambda x: add(x, 1), filter(lambda x: x % 2 == 0, ns))), number=10)
                        2.1072688089998337
                        


                        1. myrslok
                          12.01.2018 14:15

                          Благодарю.


                    1. evocatus
                      12.01.2018 18:18

                      У меня завис вот такой код:

                      [x + 1 for x in range(1000000) if x % 2 == 0]


                      1. anjensan
                        12.01.2018 18:55

                        Специально для вас не поленился и запустил на своем далеко не самом быстром телефоне (с процом Kirin 650). Печать всего списка заняла на глаз секунд пять. И всего 0.7с если не печатать, а только вычислить.

                        нотариально заверенный скриншот


                      1. MikailBag
                        12.01.2018 22:37

                        А почему печать значений генератора должна быть быстрее?
                        Единственное преимущество генератора — ленивость. Когда он используется целиком, преимущества сойдут на нет.


          1. veveve
            10.01.2018 16:38

            List comprehensions provide a more concise way to create lists in situations where map() and filter() and/or nested loops would currently be used.
            PEP 202
            www.python.org/dev/peps/pep-0202

            It has been argued that the real problem here is that Python’s lambda notation is too verbose, and that a more concise notation for anonymous functions would make map() more attractive. Personally, I disagree—I find the list comprehension notation much easier to read than the functional notation, especially as the complexity of the expression to be mapped increases. In addition, the list comprehension executes much faster than the solution using map and lambda. This is because calling a lambda function creates a new stack frame while the expression in the list comprehension is evaluated without creating a new stack frame.
            Guido van Rossum
            python-history.blogspot.ru/2010/06/from-list-comprehensions-to-generator.html


      1. domix32
        10.01.2018 14:13

        У фильтра ровно два параметра который собственно предикат проверки и итерируемый объект. Нет, мы будем писать ручками циклы с условиями и yieldить ручками вместо filter(pred, seq). Не то что бы это плохо писать фильтры ручками для понимания, но утверждать, что это упрощает чтение кода определенно не стоит.


        1. magic4x Автор
          10.01.2018 14:38

          Да боже мой ) Это пример. Самый простой. Для простого понимания. Вот вам, положите на фильтр:


          def foo(seq):
              if not isinstance(seq, bar) or baz(seq):
                  raise Exception('Bad seq')
              seen = set()
              for x in seq:
                  if egg(x) and x not in seen:
                      seen.add(x)
                      yield x

          В таком примере слишком много лишнего, чего я не собирался говорить.


          1. MikailBag
            10.01.2018 17:19

            Ну, условно,


            if not isinstance(seq, bar) or baz(seq):
                    raise Exception('Bad seq')
            seq.filter(x).dedup()


            1. magic4x Автор
              10.01.2018 19:32

              У фильтра нет метода dedup() и вы вынесли проверку уровнем выше. Т.е. вам придется ее писать всякий раз.


              1. MikailBag
                10.01.2018 19:35
                -1

                Вы написали функцию, и сказали, что ее сложно разложить в композицию простых. Я привел такой пример. Возможно, это ФП, но я не думаю, что это важно. Если считать, что функции высшего порядка — это ФП, то, так или иначе, примерно весь хороший код (благодаря dependency injection, callback, strategy) — функциональный.


                1. magic4x Автор
                  10.01.2018 20:10

                  Я сказал ровно то, что сказал: автор комментария захотел фильтр + предикат. Я предложил ему генератор, который лучше писать развернуто. Так-то compose(distinct, partial(filter, pred), validator).


    1. myrslok
      10.01.2018 15:49

      Думаю, это просто иллюстрация идеи, потом делается my_filter = filter. В неучебном коде, разумеется, не будет нового идентификатора.


  1. Fbist
    10.01.2018 10:59

    Было бы интересно, если бы кто привел результаты работы (количество действий) интерпретатора в зависимости от кода. Уверен, что баловство с функциями притормозит его % на 10. Иногда это критично.


  1. Johan
    10.01.2018 11:12

    Надеюсь, декоратор вписан в одну строку исключительно для статьи.


  1. brake
    10.01.2018 12:10
    +1

    Про Clojure полностью разделяю мнение автора, а про Python нет. Сам был в похожей ситуации, когда «вкусил ФП». Тоже вдохновился и начал выдавать подобные штуки: list(map(filterfalse(и т.д.))) — мне казалось, что так красивее и читабельнее. Однако, через пол-годика — годик, когда стал снова копаться в своем коде «функционального периода», я уже был совсем недоволен своими синтаксическими эквилибрами (ведь встроенной композиции в языке нет, а лепить свою или тащить внешние зависимости я даже под эйфорией не хотел).


    В итоге все вернул на круги своя: пайтону — пайтоново. Конечно, самое дельное из ФП, вроде избегания глобального состояния, чистые функции, маленькие функции с говорящими именами и т.п. я оставил при себе и использую в Python.


    А если хочется ФП и есть возможность — я беру в руки Clojure, программировать на нем не меньшее удовольствие, чем на Python (как будто пазлы разгадываешь).


  1. DarkByte2015
    10.01.2018 12:13
    +1

    Фишка питона в том что ему не нужно ФП из-за генераторов. Все эти filter и map легко заменяются встроенными в язык генераторами списков/словарей и т.п. И имхо нативный код всегда читается проще чем какие-то функции.

    P.S. Как ни странно для ФП лучше подходят C# и Java чем питон. Там хотя-бы есть нормальные стрелочные лямбды типа `x => x * 2`. А в питоне лямбды куда длиннее пишутся, это немного раздражает `lambda x: x * 2`. Зачем спрашивается это дурацкое слово лямбда в начале? Оно только удлинняет код. И без него понятно это лямбда… Да и стрелки вместо двоеточия в лямбдах используются в большинстве языков. Было бы проще если бы он так-сказать следовал традициям.


    1. kkirsanov2
      11.01.2018 12:02

      Нет.

      Фишка питона в том что у него весьма неудобная композиция в виде декораторов, ограниченая лямбда и неудобный (сравниявая с ML-style ФП) синтаксис для функциональщины.

      В результате «функциональный» код на нем смотриться чужеродно.
      Тут не зря LINQ вспоминают.


  1. mjr27
    10.01.2018 12:41
    +3

    Буквально пара придирок по примерам… Ну или мыслей вслух, кому как нравится.


    1. @post_processing(list)
      Вообще правила хорошего тона очень уж не рекомендуют декораторам менять тип возвращаемого значения.


    2. List comprehansion VS functools

    У list comprehension есть одна потрясающая особенность: они не прерывают контекст чтения. Одним взглядом сразу становится понятно, что здесь происходит и какой тип у нас на выходе.


    Вы пытаететесь утвержать, что


    filtered = [x for x in seq if x is not None]

    это "куча ненужной фигни" по сравнению с


    from operator import is_
    from itertools import filterfalse
    from functools import partial
    
    is_none = partial(is_, None)
    filter_none = partial(filterfalse, is_none) 
    filtered = filter_none(seq)

    Серьезно что ли? Посыл понятен, но пример-то свидетельствует о совершенно противоположном


    1. magic4x Автор
      10.01.2018 13:21
      -1

      Держите:


      from mytools import filter_none
      from itertools import chain
      
      filtered = filter_none(seq)
      filtered2 = filter_none(seq2)
      all_filtered = filter_none(chain(seq, seq2))


      1. mjr27
        10.01.2018 14:33
        +1

        Дык все равно filter_none = lambda seq: (x for x in seq if x is not None) строчки на три короче и во сколько-то раз читаемей получается


        Опять же, повторюсь, посыл понятен. Просто на таких масштабах пример выходит сомнительный. В сложных случаях тоже зачастую обычный for ... in ... получается куда более… readable


        Очень уж в python LINQ-style filtered = seq.where(r=> r is not None) на борту на хватает.


        P.S: спасибо за controlcenter :)


        1. magic4x Автор
          10.01.2018 15:28

          Где ж оно короче? ) Давайте считать, берем два массива:


          filtered = filter_none(seq)
          filtered2 = filter_none(seq2)
          all_filtered = filter_none(chain(seq, seq2))
          
          # versus
          filtered = (x for x in seq if x is not None)
          filtered2 = (x for x in seq2 if x is not None)
          all_filtered = (y for x in (seq, seq2) for y in x if y is not None)

          Правда круто бойлерплейта налетает, если сделать хотя бы два раза то же самое?


          1. mjr27
            10.01.2018 15:57

            Считать так считать ))


            # v1
            from operator import is_
            from itertools import filterfalse
            from functools import partial
            
            is_none = partial(is_, None)
            filter_none = partial(filterfalse, is_none) 
            filtered = filter_none(seq)
            filtered2 = filter_none(seq2)
            all_filtered = filter_none(chain(seq, seq2))
            
            # v2
            filtered = (x for x in seq if x is not None)
            filtered2 = (x for x in seq2 if x is not None)
            all_filtered = (y for x in (seq, seq2) for y in x if y is not None)
            
            # v3
            from itertools import chain
            
            filter_none = lambda seq: (x for x in seq if x is not None)
            filtered = filter_none(seq)
            filtered2 = filter_none(seq2)
            all_filtered1 = filter_none(chain(seq, seq2))
            
            # v4 -- для полных лентяев типа меня
            def filter_none(seq):
              return (x for x in seq if x is not None)
            
            filtered = filter_none(seq)
            filtered2 = filter_none(seq2)
            all_filtered2 =  filter_none(seq + seq2)

            Какой вариант вызывает минимум WTF в минуту?


            1. magic4x Автор
              10.01.2018 16:08

              Ура, вы написали функцию. Да, предикат повторно использовать не сможете, но сделали же по моему! :) И вот так (seq + seq2) не стоит делать. Во-первых, не работает с ленивыми, во-вторых, вы получите третий список/тапл.


  1. evseev
    10.01.2018 12:54
    +1

    Лично мне импонирует функциональное программирование. Но вот что я не совсем понимаю зачем его притягивать за уши. Ложится что-то прямо сейчас- напиши оставив комментарий. Но зачем это делать везде преодолевая трудности и усложняя жизнь коллег? Хочется ФП? Пиши на Lisp, Haskell и всех их родственниках и потомках. И тебе будет приятно и читать это будут люди, понимающие что происходит.

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


  1. nick_gabpe
    10.01.2018 13:46

    Скажите, пожалуйста, а почему в первом примере не так?

    no_none = filter(None, seq)


    1. magic4x Автор
      10.01.2018 13:48

      Это просто пример, для легкого понимания. И коли это уже второй (третий?) подобный коммент: лично я не фанат использовать None в качестве предиката. Потому что он фильтрует не только наны.


  1. MrBuha
    10.01.2018 14:35

    Это прям «слишком» простое программирование, судя по заглавному изображению.


  1. random1st
    10.01.2018 15:33

    Лично из моей практики из пакета functools  использую partial (можно каррирование делать через лямбды, но выглядит не так хорошо) и wraps для декораторов. Это понятно, относится к стандартной библиотеке и не надо прыгать по коду в поисках какого-нибудь my_filter. C таким подходом в имени функции будет столько же символов, сколько и в ее коде.


  1. amarao
    10.01.2018 18:44
    -2

    При всём уважении, вот это в продашен-коде в code review я бы завернул к чертям:


    def compose(*fns):
        init, *rest = reversed(fns)
        return lambda *a, **kw: reduce(lambda a, b: b(a), rest, init(*a, **kw))

    Почему? Вопрос читаемости чуть ниже, а вот ещё один момент: Сигнатура получившегося "очень помогает" интроспекции:


    >>> mapv
    <function compose.<locals>.<lambda> at 0x7f782f1f0400>
    >>> filterv
    <function compose.<locals>.<lambda> at 0x7f782f1f0488>

    Простите, простите, а что делает filterv?


    >>> help(filterv)
    Help on function <lambda> in module __main__:
    
    <lambda> lambda *a, **kw

    Ага, смотрим на сигнатуру. Она принимает список аргументов состоящий из позиционных и именованных аргументов. позиционные называются "a", именованные kw.


    Соответственно, мы точно можем сказать, что эта функция делает что-то с данными. Очень важное знание.


    Но давайте поробуем использовать эту функцию. Внезапно, если у нас в программе определена переменная reduce (локальная переменная!) то код даст потрясающие сайд-эффекты. Почему? Потому что в питоне нет замыканий, а попытка играть в ФЯП без замыканий обречена на унижения.


    >>> def reduce(*args):
    ...     print("BAD CODE")
    ... 
    >>> mapv([], [])
    BAD CODE

    Как же так? Неужели ваша ЧИСТАЯ функция зависит от глобального состояния? Ну куда это годится-то?


    А теперь про читаемость. Там всё просто: нечитаемо, перепишите по человечески.


    1. magic4x Автор
      10.01.2018 18:56

      Вы можете написать свой компоуз, который склеивает хелпы и собирает красивую доку, я не прошу пользоваться своей реализацией, она "примерная".


      И при всем уважении, я не стал бы работать в команде, где кто-то лезет в чужой модуль, патчит очевидные вещи и потом жалуется на нерабочий код.


      1. amarao
        10.01.2018 19:13

        Я очень извиняюсь, но ничего я не патчу.


        Если я пишу вот так вот:


        def myfunc():
            reduce = True
            other_func()

        То я не ожидаю, что моя локальная переменная повлияет на работу other_func. А вы конструируете лямбду, которая радостно использует локальную переменную reduce вместо функции.


        Это называется сайд-эффект и это прямое последствие игрищ с лямбдами вместо нормальных функций.


        1. anjensan
          10.01.2018 19:25
          +1

          Ваша локальная переменная и не повлияет.
          Переменные в интерактивном шелле глобальные (принадлежат модулю '__main__').

          Если вы напишете так, как вы показали… То ничего не произойдет.

          >>> def prod(s):
          ...   return reduce(lambda x, y: x * y, s)
          ... 
          >>> def myfunc():
          ...   reduce = True
          ...   return(prod([1, 2, 3]))
          ... 
          >>> myfunc()
          6
          

          В общем не позорьтесь.


          1. amarao
            11.01.2018 12:17

            Ваш пример немного не о том (вы делаете reduce от лямбды, а не возвращаете лямбду с reduce'ом.


            Но я тоже совершенно неправ:


            def compose(*fns):
                init, *rest = reversed(fns)
                return lambda *a, **kw: reduce(lambda a, b: b(a), rest, init(*a, **kw))
            
            def no():
                reduce=None
                return compose([],[])()
            
            no()

            Однако, при этом замыкание не настоящее, если я переопределяю reduce в глобальном пространсте, то он начинает использоваться… Я даже проверил с импортами — сохраняется ссылка на reduce в namespace модуля, где поределена функция.


            Если честно, но я не понимаю логики тут. Если reduce берётся в замыкание в момент определения лямбды, то почему её переопределение работает? Если оно не берётся в замыкание, а используется в момент выполнения лямбды, то почему оно берётся из другого namespace'а (не того, в котором выполняется)?


            Более того, это какая-то ахинея:


            no()
            Traceback (most recent call last):
              File "<stdin>", line 1, in <module>
              File "<stdin>", line 3, in no
              File "<stdin>", line 3, in <lambda>
            NameError: name 'reduce' is not defined
            
            import reduce

            … И оно принимается.


            1. anjensan
              11.01.2018 12:55

              Однако, при этом замыкание не настоящее
              Замыкаются только локальные переменные, reduce берется из глобал-скоупа.

              Если оно не берётся в замыкание, а используется в момент выполнения лямбды, то почему оно берётся из другого namespace'а (не того, в котором выполняется)?
              Потому что именно так работает Python. Можете наконец почитать туториал. Или его перевод

              Более того, это какая-то ахинея:
              В Python3 функцию reduce убрали из глобальных. Надо ее импортировать из functools.


              1. amarao
                11.01.2018 13:45

                Вы знаете, я не поленился почитать, и я там совершенно ничего не увидел про это замечательное «замыкаются только локальные переменные». Что это происходит, я понял, но откуда, кроме как эмирически, это можно понять?


                1. anjensan
                  11.01.2018 13:57

                  Вы знаете, я не поленился почитать
                  Перечитайте еще раз.

                  Вот вам прямые цитаты из приведенной мной ссылки:
                  В любой момент во время выполнения существует как минимум три вложенных области видимости, чьи пространства имён доступны прямым образом: самая внутренняя[53] область видимости (по ней поиск осуществляется в первую очередь) содержит локальные имена; пространства имён всех объемлющих [данный код] функций, поиск по которым начинается с ближайшей объемлющей [код] области видимости; область видимости среднего уровня, по ней следующей проходит поиск и она содержит глобальные имена текущего модуля; и самая внешняя область видимости (заключительный поиск) — это пространство имён, содержащее встроенные имена.

                  В вашем примере 'reduce' не объявлена локальной переменной, потому ресолвится в глобальную. Вот тут нету замыкания:
                  def mr(op):
                     def f(x): 
                         return reduce(op, x)
                     return f
                  

                  А вот тут есть
                  def mr(op):
                     def f(x): 
                         return r(op, x)
                     r = reduce
                     return f
                  


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


                  1. amarao
                    11.01.2018 14:09

                    Мне немножно тяжело с русским, но ок.

                    Где из процитированного вами сказано, что глобальные переменные в замыкание не попадают?

                    То есть мой вопрос сейча звучит так: где написано про то, что глобальные имена не попадают в замыкания?

                    Ещё интереснее вопорс: меня тут в соседнем треде убеждали, что в питоне таки есть замыкания. Так они есть, или их нет?


                    1. anjensan
                      11.01.2018 14:32

                      Написано

                      It is important to realize that scopes are determined textually: the global scope of a function defined in a module is that module’s namespace, no matter from where or by what alias the function is called. On the other hand, the actual search for names is done dynamically...
                      Поэтому глобальные значения просто не могут захватываться через замыкание (ибо их может просто не существовать на момент объявления функции).

                      Ещё интереснее вопорс: меня тут в соседнем треде убеждали, что в питоне таки есть замыкания. Так они есть, или их нет?
                      Более того, именно я утверждал, что замыкания есть.
                      Болеее того, в сообщении, на которое вы ответили… я тоже писал, что в питоне есть замыания. Вы меня пытаетесь троллить?

                      Если так, то предлагаю прекратить сею бесцельную дискуссию.
                      Если нет — искренне прошу, не занимайтесь «code review продашен-кода на Python».


    1. anjensan
      10.01.2018 19:00
      +1

      Внезапно, если у нас в программе определена переменная reduce (локальная переменная!) то код даст потрясающие сайд-эффекты. Потому что в питоне нет замыканий, а попытка играть в ФЯП без замыканий обречена на унижения.
      При всем уважении, но ваши знания Python подхрамывают. Замыкания вполне себе есть, хоть и «read-only» по умолчанию (что исправляется nonlocal).

      И даже аттрибут __closure__ у функций есть, что как бы намекает
      >>> def f(): x=1; return lambda: x
      ... 
      >>> f().__closure__
      (<cell at 0x7f886ad98558: int object at 0x556d5d395e80>,)
      >>> f().__closure__[0].cell_contents
      1
      


      1. amarao
        10.01.2018 19:16

        О, оно там есть? Странно, почему тогда мой пример использует локальный reduce, вместо reduce, определённого на момент создания лямбды?


        1. magic4x Автор
          10.01.2018 19:29

          Потому что вы это сделали в том же скопе (консоль, модуль). Ваш пример нежизнеспособен:


          reduce = lambda x: None
          from funcy import compose
          compose(str, int)(3.2)
          '3'



  1. anjensan
    10.01.2018 18:57