«Простое» программирование на python


    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


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

    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

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

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

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


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


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

        • +7
          Когда у меня будет падать код, удаляющий наны из списка, я посыплю голову пеплом и пойду копать канавы. Все же читать проще бойлер-плейт, как вы называете, чем вводить кучу новых идентификаторов, тем самым засирая стек. Не отрицаю, возможно в частных случаях это делает код более стройным, но вот весьма спорно то, что это стоит использовать везде.
          • 0
            ох не зарекайся насчет пепла и канав
          • +2
            Так пишите функцию, кто вам запрещает? Непонятно только зачем все эти функциональные навороты, когда проблема аналогично решается выносом императивно «бойлерплэйта» в отдельную функцию. А еще «pluck» — ужасное имя для функции, лучше бы над именованием задуматься, чем над тем, как извратиться в написании тривиального кода.
            • 0
              В компьютерных науках есть только две сложные вещи: инвалидация кэша и именование всяких штук )) Второе!
              • 0

                Ну в lodashjs тоже pluck.

              • –1

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

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

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

              • +16

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

                • –3

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

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

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


                      from mytools import mytool
                      
                      myresult = mytool(mydata)

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

                      • +1

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

                  • +1
                    Для функциональщиков это может быть и проще, но для императивщиков… Я лично глаза мозги сломал пока разложил всё это мысленно в обычные императивные функции, понял как это работает и понял что лично я так никогда делать не буду :-)
                    • +1
                      Согласен. Сам стараюсь писать так, чтобы те кто с языком не знакомы могли понимать как код работает. В ущерб самолюбию. Это моя интерпретация догмы «Explicit is better than implicit» — сторониться неочевидного.

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

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

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

                      • +1
                        Lasciate ogni speranza, voi ch’entrate
                      • 0
                        вот вот, дальше не читал.
                      • +5

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


                        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)

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

                        • 0
                          Почему Вы противопоставляете 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
                          • 0
                            В питоне 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
                            Можно даже чуть короче: filter(None, seq)
                            • 0

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

                          • +3
                            Для меня идеальный код, это когда его читаешь как текст.
                            И кстати, зачем в первом примере pred = bool глобальная переменная?
                            • +2
                              Полагаю автор хотел довести нас к прозрению малыми шагами, а вышло как всегда: кто и без того понимал суть — стали придираться; кому оно не сильно надо — всё равно споткнулись и оставили; наверно, единицы кому зашло с благодарностью улыбнулись и пошли дальше.
                              Автор молодец, он придумал хорошие примеры, обосновал их полезность, разжевал и отполировал изложение. Кому-то будет полезно наверняка. Я лично почерпнул лишь некоторые тонкости манеры изложения, и оно того стоило, знаете ли. Какой-нибудь пример обязательно пригодится, когда в очередной раз придётся рассказывать молодым коллегам основы.
                            • +12
                              Извините, но не нужно из python делать clojure (извините, lisp). Да, чистые функции предсказуемы, тестируемы, хорошо параллелятся. Да, функции высшего порядка более универсальны. Да, map reduce легче читается, чем for if for for.
                              Но все же надо знать меру, и не нужно вводить кучу новых функций ради композиции и каррирования, не нужно более читаемые for if for for заменять совершенно нечитаемыми вложенными скобками, не нужно в худших традициях ФП вместо трансформации списков заниматься их копированием.
                              Возьмите лучшее из обоих миров и спокойно пишите понятный код, не наживая себе хаскель головного мозга, не беспокоясь о том, что где-то не слишком Функционально.
                              Ну а за попытку из изначально императивного (знаю, мультипарадигменного, но все же) языка сделать чисто функциональный — статье плюс.
                              • +1
                                Ой, не сгущайте краски, коллега. Вы, конечно правы, но никто никого, как мне кажется, не пытается заставить превращать один язык в другой. Понимать концепцию всегда полезно и для новичков, например, через питон это сделать. может быть будет иногда лучше.
                                Я тоже с трудом вижу где бы такой перефункционализированный подход улучшил питоновский код в обыденной жизни. Однако, как говорится, хорошо подобранным примером можно доказать всё что угодно.
                                Давайте напряжемся и придумаем за автора (как адвокаты дьявола) пример в его пользу. Это же интересно.
                                Мне приходит на ум что-то вроде задач сложной настраиваемой обработки потоков данных, когда набор преобразований, применяемых к потоку, требуется сделать кастомизируемым, прозрачным и поддающимся контролю. Я про те самые случаи. когда ООП с его состояниями побочными эффектами плавно превращается в геморрой из фабрик, куч, пуллов, очередей и прочего. Не знаю даже. Подумаю еще=).
                                • 0

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


                                  @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 больше. И дело тут не в размере, а в том, что используя одни и те же инструменты (и понимая как они работают), вам не нужно читать и писать код, который делает то же самое, но конкретно тут и конкретно так каждый раз.


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

                              • +17
                                На улице 2018… А всё как 8 лет назад — по основам функционального кода в питоне пишут статьи, а в комментах идет срач, о том что императивный стиль понятнее…
                                • –1
                                  Не понял зачем писать собственные варианты фильтра, когда в области видимости без импортов уже есть `map`, `filter` и `reduce`.
                                  • 0

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

                                    • –1

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

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

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

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

                                          • +2

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

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

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


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


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

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

                                                • –1
                                                  Лучше использовать по возможности круглые скобочки, чтобы получить 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

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


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

                                                    • 0
                                                      Я прекрасно понимаю разницу между списком и генератором. Просто имел в виду, что если можно использовать генератор, то лучше использовать генератор, потому что его создание «дешевле».
                                                    • 0
                                                      На самом деле не обязательно.
                                                      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
                                                      
                                                      • 0

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


                                                        timeit(lambda: sum(map(lambda x: x + 1, filter(lambda x: x % 2 == 0, ns))), number=10)
                                                        • 0
                                                          Я отвечал 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
                                                          
                                                      • 0
                                                        У меня завис вот такой код:
                                                        [x + 1 for x in range(1000000) if x % 2 == 0]
                                                        • 0
                                                          Специально для вас не поленился и запустил на своем далеко не самом быстром телефоне (с процом Kirin 650). Печать всего списка заняла на глаз секунд пять. И всего 0.7с если не печатать, а только вычислить.
                                                          нотариально заверенный скриншот


                                                          У вас явно какой-то неправильный питон :)
                                                          • 0

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

                                                • +1
                                                  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

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

                                                • 0

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


                                                  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

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

                                                  • 0

                                                    Ну, условно,


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

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

                                                      • –1

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

                                                        • 0

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

                                              • 0

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

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

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

                                                  • +3

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


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


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

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

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

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

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

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


                                                        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

                                                          Держите:


                                                          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

                                                            Дык все равно 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 :)

                                                            • 0

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


                                                              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)

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

                                                              • 0

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


                                                                # 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 в минуту?

                                                                • 0

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

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

                                                          PS: я за то, что бы любой код, который хотя-бы теоретически может быть прочитан другим человеком, был или самоочевиден или был щедро сдобрен комментариями. Хотя-бы потому, что этим другим человеком можешь быть ты сам, но не выспавшийся, больной или мучимый похмельным синдромом и просто не помнящим что тут вообще происходит.
                                                          • –1
                                                            Скажите, пожалуйста, а почему в первом примере не так?
                                                            no_none = filter(None, seq)

                                                            • 0

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

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

                                                                  При всём уважении, вот это в продашен-коде в 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

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


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

                                                                  • 0

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


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

                                                                    • 0

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


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


                                                                      def myfunc():
                                                                          reduce = True
                                                                          other_func()

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


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

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

                                                                        Если вы напишете так, как вы показали… То ничего не произойдет.
                                                                        >>> def prod(s):
                                                                        ...   return reduce(lambda x, y: x * y, s)
                                                                        ... 
                                                                        >>> def myfunc():
                                                                        ...   reduce = True
                                                                        ...   return(prod([1, 2, 3]))
                                                                        ... 
                                                                        >>> myfunc()
                                                                        6
                                                                        

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

                                                                          Ваш пример немного не о том (вы делаете 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

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

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

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

                                                                            Более того, это какая-то ахинея:
                                                                            В Python3 функцию reduce убрали из глобальных. Надо ее импортировать из functools.
                                                                            • 0
                                                                              Вы знаете, я не поленился почитать, и я там совершенно ничего не увидел про это замечательное «замыкаются только локальные переменные». Что это происходит, я понял, но откуда, кроме как эмирически, это можно понять?
                                                                              • 0
                                                                                Вы знаете, я не поленился почитать
                                                                                Перечитайте еще раз.

                                                                                Вот вам прямые цитаты из приведенной мной ссылки:
                                                                                В любой момент во время выполнения существует как минимум три вложенных области видимости, чьи пространства имён доступны прямым образом: самая внутренняя[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
                                                                                


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

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

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

                                                                                  Ещё интереснее вопорс: меня тут в соседнем треде убеждали, что в питоне таки есть замыкания. Так они есть, или их нет?
                                                                                  • 0
                                                                                    Написано
                                                                                    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
                                                                      Внезапно, если у нас в программе определена переменная 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
                                                                      

                                                                      • 0

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

                                                                  • 0

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