Пайпы, the pythonic way

    Одни питонисты любят код читаемый, другие предпочитают лаконичный. К сожалению, баланс между первым и вторым — решения по-настоящему изящные — редко случается встретить на практике. Чаще стречаются строки вроде
    my_function(sum(filter(lambda x: x % 3 == 1, [x for x in range(100)])))
    
    Или четверостишья а ля
    xs = [x for x in range(100)]
    xs_filtered = filter(lambda x: x % 3 == 1, xs)
    xs_sum = sum(xs_filtered)
    result = my_function(xs_sum)
    
    Идеалистам же хотелось бы писать как-то так
    result = [x for x in range(100)] \
        | where(lambda x: x % 3 == 1)) \
        | sum \
        | my_function
    

    Не в Питоне?

    Простую реализацию подобных цепочек не так давно предложил некий Julien Palard в своей библиотеке Pipe.

    Начнем сразу с примера:
    from pipe import *   
    [1,2,3,4] | where(lambda x: x<=2)
    #<generator object <genexpr> at 0x88231e4>
    

    Упс, интуитивный порыв не прокатил. Пайп возвращает генератор, значения из которого еще только предстоит извлечь.
    [1,2,3,4] | where(lambda x: x<=2) | as_list
    #[1, 2]
    

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

    Как видим, источником данных для пайпов в примере стал простой список. Вообще же говоря, использовать можно любые итерируемые (iterable) сущности Питона. Скажем, «пары» (tuples) или, что уже интересней, те же генераторы:
    def fib():
        u"""
        Генератор чисел Фибоначчи
        """
        a, b = 0, 1
        while 1:
            yield a
            a, b = b, a + b
            
    fib() | take_while(lambda x: x<10) | as_list
    #0
    #1
    #1
    #2
    #3
    #5
    #8
    
    Отсюда можно извлечь несколько уроков:
    1. в пайпах можно использовать списки, «пары», генераторы — любые iterables.
    2. результатом объединения генераторов в цепочки станет генератор.
    3. без явного требования (приведения типа или же специального пайпа) пайпинг является «ленивым» в том смысле, что цепочка есть генератор и может служить бесконечным источником данных.

    Разумеется, радость была бы неполной, не будь у нас легкой возможностисоздавать собственные пайпы. Пример:
    @Pipe
    def custom_add(x):
        return sum(x)
    [1,2,3,4] | custom_add
    #10
    
    Аргументы? Легко:
    @Pipe
    def sum_head(x, number_to_sum=2):
        acc = 0
        return sum(x[:number_to_sum])
    [1,2,3,4] | sum_head(3)
    #6
    
    Автор любезно предоставил достаточно много заготовленных пайпов. Некоторые из них:
    • count — пересчитать число элементов входящего iterable
    • take(n) — извлекает из входного iterable первые n элементов.
    • tail(n) — извлекает последние n элементов.
    • skip(n) — пропускает n первых элементов.
    • all(pred) — возвращает True, если все элементы iterable удовлетворяют предикату pred.
    • any(pred) — возвращает True, если хотя бы один элемент iterable удовлетворяют предикату pred.
    • as_list/as_dist — приводит iterable к списку/словарю, если такое преобразование возможно.
    • permutations(r=None) — составляет все возможные сочетания r элементов входного iterable. Если r не определено, то r принимается за len(iterable).
    • stdout — вывести в стандартный поток iterable целиком после приведения к строке.
    • tee — вывести элемент iterable в стандартный поток и передать для дальнешей обработки.
    • select(selector) — передать для дальнейшей обработки элементы iterable, после применения к ним функции selector.
    • where(predicate) — передать для дальнейшей обработки элементы iterable, удовлетворяющие предикату predicate.
    А вот эти поинтересней:
    • netcat(host, port) — для каждого элемента iterable открыть сокет, передать сам элемент (разумеется, string), передать для дальнейшей обработки ответ хоста.
    • netwrite(host, port) — то же самое, только не читать из сокета после отправления данных.
    Эти и другие пайпы для сортировки, обхода и обработки потока данных входят по умолчанию в сам модуль, благо создавать их действительно легко.

    Под капотом декоратора Pipe


    Честно говоря, удивительно было увидеть, насколько лаконичен базовый код модуля! Судите сами:
    class Pipe:
    
        def __init__(self, function):
            self.function = function
    
        def __ror__(self, other):
            return self.function(other)
    
        def __call__(self, *args, **kwargs):
            return Pipe(lambda x: self.function(x, *args, **kwargs))
    
    Вот и все, собственно. Обычный класс-декоратор.

    В конструкторе декоратор сохраняет декорируемую функцию, превращая ее в объект класса Pipe.

    Если пайп вызывается методом __call__ — возвращается новый пайп функции с заданными аргументами.

    Главная тонкость — метод __ror__. Это инвертированный оператор, аналог оператора «или» (__or__), который вызывается у правого операнда с левым операндом в качестве аргумента.

    Получается, что вычисление цепочки начинается слева направо. Первый элемент передается в качестве аргумента второму; результат вычисления второго — третьему и так далее. Безболезненно проходят по цепочке и генераторы.

    На мой взгляд, очень и очень элегантно.

    Послесловие


    Синтаксис у такого рода пайпов действительно простой и удобный, хотелось бы увидеть что-то подобное в популярных фреймворках; скажем, для обработки потоков данных; или — в декларативной форме — выстраивания в цепочки коллбеков.

    Единственным недостатком именно реализации являются довольно туманные трейсы ошибок.

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

    Подробнее
    Реклама
    Комментарии 81
    • +1
      супер
      • –3
        Да уж, всё гениальное просто. Но не всё простое гениально)
      • +5
        Утрированный пример на самом деле решается так:
        my_function(sum(x for x in xrange(100) if x % 3 == 1))

        По сравнению с:
        [x for x in range(100)] | where(lambda x: x % 3 == 1) | sum | my_function
        • +2
          Неправда ваша, было бы так:
          range(100) | where(lambda x: x % 3 == 1) | add | my_function
          • +1
            Почему моя, если такой код в статье?
            • 0
              Действительно… в статье пример какой-то странный…
              • 0
                пример в статье высосан из пальца, признаюсь. Стоило порыться в рабочем коде и найти что-нибудь эдакое…
          • +2
            Пайпы читать проще и редактировать. Даже если они и длиннее выглядят. Поток данных проще: слева направо. А в Вашем примере надо сначала забежать глазами внутрь скобок, потом обратно.
            • +2
              total = sum(x for x in range(100) if x % 3 == 1)
              result = my_function(total)

              Читаемо? Даже очень. Лично я не люблю костыли. Пайпы это хорошо только в баше. И это НЕ pythonic way.
              • 0
                Ну, а теперь представьте, что нужно связать около 10-ка функций. Будет очень громоздко. А чем перегрузка операторов не pythonic way? Вроде же, это у них там стандартный механизм… Ну. Насколько мне известно…
                • +5
                  pythonic — это не значит, что нужно писать, используя все средства языка для достижения цели. Pythonic — это как бы одновременно код должен быть простой, незагрязнëнный и понятный.
                  • +1
                    Ну. Это же не объективные понятия: простой, незагрязнённый и понятный. Кому-то pipe'ы понятнее и проще: зачем я должен придумывать имена временных переменных, если за меня это может сделать библиотека. Это проще, на мой вкус. И тут нет призыва использовать все средства. Используется одно из средств, удобным во многих ситуациях способов. И на самом деле, тут ничего нестандартного и страшного нет, потому что вот этот pipe — вполне себе чуть ограниченная воплощение стандартной концепции монады, которая считается хорошим инструментом программирования.
                    • +2
                      Ну, кроме того, в питоне не принято абьюзить синтаксис — это принято в хаскеле. Точно так же у меня сейчас в коде рабочего проекта есть реализация стрелок на питоне, которая использует >>. Мало кто выкупает, что и как там происходит. Я б не делал такого ради любви к людям.
                  • +1
                    Есть один момент — обычно, если есть около 10-ка функций идущих подряд, и это независимые функции, а не запрос, как в примере(select… where ...), то при отладке всегда нужно проверить результат каждого промежуточного вызова. И я, лично, вряд ли бы писал сначала по одному:
                    b=fun1(a)
                    c=fun2(b)
                    а в релизном варианте переписывал в виде с = fun1(a) | fun2()
                    Во-первых долго, во-вторых, мало ли, вдруг потом ещё придется мне, или кому-то другому, отлаживать программу.
                    • 0
                      Не надо ничего переписывать. Тестируйте каждый пайп в отдельности, а потом всю цепочку в совокупности. Привет юниттестам! В этом смысле никакой разницы с простыми функциями.

                      • 0
                        Это прикольно, но это не python way, это ruby way.
                        • 0
                          Чё-то это уже странные утверждения, imho. Если это написано на Python, то какой нафиг Ruby way? По-вашему, что? Вообще никакую библиотеку нельзя использовать, потому что там какие-то функции понаписаны, и никто не поймёт, что происходит, читая код, в котором они используются.

                          Мда… Фанаты — люди странные. Сделан классный механизм, стандартными средствами их любимого языка… Радоваться надо, разве нет? А то, что какие-то там проблемы могут быть, да… Могут. Но это повод библиотеку развивать, а не трясти перед ней python way'ем.

                          Эх :( Вы убили во мне желание воспользоваться в следующем проекте Python'ом. Какое-то у него Дао странное. Буду юзать Lua.
                          • +1
                            Ну какие фанаты, ну что это за разбрасывание лейбами? «Любители метапрограммирования — люди странные. Можно написать так просто, так нет же, дай сделать что-нибудь позапутаннее.»

                            Я вот сейчас поддерживаю большую довольно кодовую базу на питоне (80к+ SLoC, для питона это — очень много), и надо сказать, что любая попытка внести маленькие приятные штуки оказывается приятной только для автора. Или, другими словами, задолбало находить странные потуги чуваков на метапрограммирование. Хочется оторвать руки и пришить что-то, что будет выдавать поддерживаемый код, а не отрыжки мамонта.

                            Да, можно и с метапрограммированием писать поддерживаемый код. Вопрос лишь в самодисциплине, конечно. И за значения по умолчанию я принимаю то, что у большинства самодисциплина плоха.
                            • 0
                              Метапрограммирование? Это что такое? Декораторы? Метаклассы? Обычная весьма вещь, в любом коде встречается, множество фреймворков это использует.

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

                              Что есть великолепный программист? Тот, кто может, усвоив правила большой системы, увеличить внутреннюю корректность, не нарушая цельности.

                              Что есть плохой программист? Тот, кто использует приемы, идущие в разрез с архитектурой системы; и не принимающий общие решения.

                              И метапрограммирование не имеет к этому никакого отношения.

                              • 0
                                Я бы сказал, что метапрограммирование — это построение DSL. Внутренних или внешних.
                                А декораторы-метаклассы — всего лишь детали реализации. Подмножество используемых для метапрограммирования инструментов.
                                • 0
                                  Вероятно, это прозвучит забавно, но… Разве любое программирование API, уж тем более вблизи парадигмы ООП, — не то ли самое построение DSL?

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

                                  И вообще буду строгим :). Признаться, после ознакомления с «DSL» на примере Scala, Haskell и Ruby, я перестал понимать, откуда взялась в специальной прессе и блогосфере такая шумиха вокруг этого неопределенного термина; так же посмеиваюсь над теми, кто делает идола из ООП-подхода. Подумаешь, догадались данные складывать в кучку с функциями для работы с ними! Полиморфизм — просто издержки жесткой системы типов; инкапсуляция — просто хорошая практика программирования; наследование — не самый удачный способ менять поведение объектов класса.

                                  И все. :)
                                  • 0
                                    Лично у меня эти понятия различаются «в голове».
                                    Можно делать ООП на С. А можно и на С++ писать «с классами», но без ООП.
                                    DSL и ООП предполагают несколько отличающийся способ мышления.

                                    Т.е. если я делаю внутренний DSL на Питоне — то использую метаклассы, декораторы, перегрузку операторов. Главное — думаю о получившейся конструкции как о создании нового микроязыка (не выходящего за синтаксис Питона, конечно). Вот и вся разница.
                                    • 0
                                      Но согласитесь, понятия различаются очень условно! Можно даже сказать, что DSL — это просто что-то вроде фразы «надо стремиться говорить на языке целевой области». А уж средства… ООП, функциональный подход, метапрограммирование, макросы (даже в Си/Си++ что-то похожее на DSL с их помощью вытворяют )… Кто во что горазд.

                                      В рамках концепции ООП очень даже можно писать DSL. Скажем, тот же злосчастный джанговский ORM; многие другие реализации ActiveRecord — очень даже себе DSL.

                                      Для меня твердой грани давно уже нет; видать слегка подпортил себе мышление обилием опробованных языков :-) DSL — это просто одна из задач, что иногда встречается.
                    • 0
                      Хм. Особых проблем с отладкой pipe'ов в том же Bash нет. А вот эти Python-пайпы всяко более удобны.
                      • 0
                        Удобны более чем что? Чем непайпы?
                        В баше проблем нет, потому что нет и многого другого. Перегрузки операторов, например.
                        • 0
                          В bash как языке есть множество своих проблем: архаичный синтаксис, много избыточности и бла-бла-бла.

                          С другой стороны, в bash как инструменте для работы в командной строке есть много плюсов: простота, хорошая работа с текстом. Да и просто обмен данными через stdin/stdout, на мой взгляд — это блестяще. не говоря уже о десятилетиях добрых *никсовых традиций и лучших практик.

                          Однако, здесь речь идет прежде всего об использовании хорошо знакомого любым программистам синтаксиса для построения схожей абстракции на языке общего назначения…

                          Лично мне это интересно прежде всего в качестве элемента более крупной фреймворка или либы для поэтапной обработки данных, но никак не в качестве замены баша.
                          • 0
                            Да Bash тут только при том, что с отладкой проблемы не такие уж и большие возникают.

                            А с синтаксисом… Ну, я ещё повторюсь: человек хочет сделать стандартную математическую абстракцию — композицию функций. Хочет выразить её через оператор, чтобы не писать f.compose(f1).compose(f2).compose(f3)… * использовать не разумно, потому что она часто встречается в коде. А |, известно из Bash, вполне себе уживается с редко используемым битовым or. Всё, imho, рационально.

                            Вы же, наверное, не критикуете библиотеки за то, что в них перегружают арифметику для работы с матрицами или длинными числами. Или когда + используется для конкатенации строк, или когда то же | используется для записи грамматических правил.
                            • 0
                              Пытаюсь вспомнить, в каком коде я видел f.compose(f1).compose(f2).compose(f3)… *
                              Нет, не вспоминается.
                              • 0
                                Зато что-то вроде
                                f.compose(
                                    f1,
                                    f2,
                                    f3(arg)
                                )
                                

                                писал не раз. Обратите внимание: композиция функций с использованием круглых скобок, таких привычных и родных.
                                • 0
                                  Ай… Право же… Какой-то странный критерий оценки. Почему привычность должна быть приоритетнее удобства? Вероятно, нам с Вами друг друга просто не понять.
                                  • 0
                                    Нет. Ну в самом деле. А почему вы не настаиваете на том, чтобы вместо A * B + C писать: A.mul(B).add©? Привычнее же. Сплошные скобки… Прямо Lisp — классика.
                                    • 0
                                      Знаете, есть такая штука: принцип наименьшего удивления. Архитектурные решения, разработанные с использованием этого принципа, оказываются удобными. Так вот, использовать арифметические операторы для арифметических же действий — тоже привычно еще с детского садика.
                            • 0
                              Не чем 'непайпы', а как способ композиции функций. При чём тут перегрузка операторов — не очень понятно. Есть в математике такая стандартная операция — композиция. Философские причины того, почему в языке может быть перегружен оператор * для умножения матриц, а оператор | не может быть перегружен для композиции функций — лично для меня загадочны.
                              • 0
                                Перегружать — можно. Делайте на здоровье.

                                Используйте оператор | на полную катушку. Через год-два посчитайте, сколько раз за это время эта штука вам понадобилась. Перечитайте еще раз код, который эти пайпы использовал. И напишите success story, как все было хорошо и здорово, что вы уже не представляете себе жизни без чудо-механизма.
                                • 0
                                  Не буду уже. Уже начал писать на Lua. А вы видели success story про умножение матриц при помощи *? Было бы интересно почитать, что там люди пишут.
                                  • 0
                                    Я читал много статей о numpy. Были среди них и success story.
                • 0
                  Это действительно гениально, причем и как идея, так и реализация! Я в восторге!
                  Обязательно начну использовать у себя.
                  • +3
                    Не надо, пожалейте тех, кто будет работать с вами.
                    • 0
                      Никто не будет, я буду использовать в СВОИХ скриптах, которыми пользуюсь только я. Конечно же нельзя использовать pipe в проектах, код которых будет использовать кто-то ещё.
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • 0
                          Если нельзя использовать в таких проектах, то это как бы звоночек. Может лучше вообще не использовать?
                  • 0
                    Главное — создать свой объект, т.к. в Питоне нельзя расширять предопределённые типы :(((. А трубы — это по практически то же, что и вызов методов:

                    [1,2,3,4] | where(lambda x: x<=2) | as_list

                    легко реализуется «питоническим» способом как:

                    KribleKrable([1,2,3,4]).where(lambda x: x<=2).as_list()
                    • 0
                      Есть неприятный нюанс. В каком namespace будет искать имена ваш KribleKrable?
                      Я имею в виду: как найдутся функции where и as_list из примера?
                      • 0
                        where и т.п. — методы объекта, возвращаемого функцией или конструктором KribleKrable. А сама KribleKrable ищется, очевидно, в том же пространстве имён, что и where из трубного примера.
                        • 0
                          «трубный» пример будет последовательно искать в локальном, глобальном и builtin пространствах имён.
                          Т.е. можно писать свои «трубы», оборачивая их декоратором @Pipe.
                          Наличие декоратора некрасиво, ну да ладно.

                          KribleKrable же должна явно содержать все доступные методы. Можно, конечно, выкрутиться.

                          1. Перегрузить __getattribute__ (или __getattr__ — не важно).
                          2. Хакнуть фрейм и вытащить f_globals/f_locals.
                          3. Эмулировать путь поиска имени.

                          Но это запутанная процедура, не делающая прочтение кода яснее — слишком логика отличается от привычной. Мне не нравится. Впрочем, «трубный» пример не нравится тоже.
                    • +16
                      Сомнительный запашок у получающегося кода.

                      Перегрузка операций (любых) — не самый лучший способ построения каскадов генераторов.
                      Из-за этой перегрузки в записи возможны досадные ошибки.

                      Понятность снижается (а должна бы увеличиваться).
                      Оператор | может встречаться и в привычном контексте битовых операций.
                      Здесь же придется внимательно смотреть: это pipe или нет?
                      Если, скажем, numpy перегружает операторы для матриц — там ясна предметная область и не происходит
                      её перекрытия со стандартной семантикой. Если складываются два числа — они делают это по правилам элементарной арифметики. Две матрицы — тоже ничего нового.
                      В sqlalchemy для SQL запросов перегружаются все битовые операции.
                      При этом биты для чисел и запросы для SQL разведены по разным углам, очень редко пересекаясь в коде. По выражению практически всегда легко понять, где кто.
                      Здесь же | работает как битовый для чисел и pipe в стиле bash если самый правый операнд оказался типа Pipe.
                      Семантика разная, и две семантики легко могут пересекаться. Это неприятно.

                      Еще одно: эту штуку крайне легко сломать.
                      import pipe
                      
                      class L(object):
                          def __init__(self, v):
                              self.v = v
                      
                          def __or__(self, r):
                              return 'abra'
                      
                      print L(range(5)) | pipe.as_list
                      

                      Вроде бы класс L вполне невинен. Только, на беду, содержит оператор __or__, приоритет которого выше чем у
                      __ror__. Нехорошо…

                      Не все возможные занятные выкрутасы стоит использовать.

                      Сугубо личное мнение. Если кто не согласен — навязывать свое мнение не буду.
                      Раньше мне тоже нравились подобные полеты мысли: чем замудрённей — тем интересней.
                      Сейчас скорее признаю предлагаемый код unpythonic.
                      • 0
                        Согласен с вашими доводами, конечно нужно знать, что делаешь и в серьезных проектах такое использовать нельзя.
                        Но это же красиво! Это «just for fan»! Почему бы и не поиспользовать это в своих скриптах? На мой взгляд, код многих мои скриптов с использованием pipe будет читаться и выглядеть гораздо проще и красивее, но это лишь мое мнение.
                        • 0
                          Если бы мы занимались увлекательным делом «напиши как можно хитрее» — тогда да, метод хорош.
                          Оно только выглядит проще — а понимается на самом деле сложнее чем запись «в лоб». Мы еще не переходили к обработке ошибок и чтению traceback — вот где поле непаханое.

                          И всё же повторюсь еще раз: предлагаемый подход ненадежен по построению.
                          Можно поиграть в занятную игру: придумывание кода, который валит библиотеку или заставляет ее работать «странно». Поверьте, этого кода много. У меня сразу нарисовалось несколько безобидных на первый взгляд вариантов. В примере я привел только самый короткий и очевидный.
                          • +2
                            imho, дело не в написании хитрее. Люди ищут способы писать красивее.

                            Вот я уже не помню когда арфметическое или видел в живом коде. Как-то битовые операции остались в моём далеком прошлом :)
                            И перегрузку или тоже не припоминаю.

                            Хотя, всё что вы сказали — да, не добавляет радости к этой красоте.

                            С другой стороны, питон не призван быть супернадежным инструментом, многие вещи построены на доверии и предположении, что разработчик понимает, что он делает. А если есть сомнения — сомнений быть не должно. Команда внутри себя может дjговориться, например, что в рабочем кода нельзя использовать pipe, и делов-то.
                            • +1
                              Множества, которые set/frozenset, тоже не используете?

                              > Команда внутри себя может дjговориться, например, что в рабочем кода нельзя использовать pipe, и делов-то.
                              Простите, потерял нить разговора. Вы предлагаете применять pipe исключительно в нерабочем коде?
                              • +1
                                множества так сходу не примопню, нужно разработчиков спросить :)

                                эх, мне казалась мысль очень цельной.
                                Ок, коротко: pipe вообще не факт, что для использования в реальных проектах.
                                Это может быть просто как иллюстрация того, что может язык (и насколько просто он это может). Как исследование в каком направлении языку двигаться.
                                В конце концов — есть люди у которых ipython в качестве шела :)
                                • 0
                                  Кстати говоря, о множествах.

                                  Вот если честь по чести, то эти самые множества используют тот же самый механизм переопределения __or__/__ror__; и ничего. Никто не кричит, что — О БОГИ! — программисты переопределили какую-то там побитовую операцию для каких-то там специальных типов.

                                  Ну и что? В конце концов, реальный рабочий код пайп можно было бы разбавить нужными проверками и так далее.

                                  Я все же утверждаю, что пайпы вполне работоспособны именно в качестве элемента какого-либо фреймворка, где можно твердо очертить область их применения.
                                  • 0
                                    Давайте поразмышляем, как сделать пайпы (с перегрузкой | и прочими плюшками) более безопасными в использовании.
                            • +5
                              «just for fan»? Для вентилятора, пожалуй, в самый раз.
                              Рекомендую к просмотру и осмыслению — Ларри Гастингс: The Python That Wasn't.
                            • 0
                              Соглашусь с вами. Лишние выкрутасы. :)
                              • 0
                                Эмс… Не понятно, а в каком месте пайпы могут перемешаться с битовым or? Если утверждается, что пайпы можно только с пайпами объединять или с генераторами. То есть, пайп будет между пайпов, а битовый or будет внутри каких-нибудь скобочек внутри lambda или ещё где… В каком месте они смешаются?
                                • +1
                                  Может быть:
                                  [1, 2, 3] | set([3, 4])
                                  • +2
                                    Пайпы хотелось бы объединять только с пайпами или генераторами.
                                    А на деле запрета нет, и глюкавый код написать вполне можно.
                                    Более того, такую проверку (забудем про тормоза на время) не очень-то и вставишь.
                                    Чтобы все работало, нужно «генератор» заменить на «итерируемый объект» — иначе пример со списком работать не будет.
                                    А итерируемость — слишком широкое понятие. Строка, например, подходит.
                                    Если вспомнить, что оператор | переопределен в том числе и для множеств — проблема перестает быть академической.
                                    Чудить можно с размахом.
                                    Самый простой пример (он ничего не ломает, но слегка обескураживает):
                                    from pipe import as_list
                                    a = {1, 2, 3}
                                    b = {4, 5}
                                    print a | b | as_list
                                    
                                  • 0
                                    Только, на беду, содержит оператор __or__, приоритет которого выше чем у
                                    __ror__. Нехорошо…

                                    Опс. Нифига себе. Спасибо.
                                    • 0
                                      Кстати, вариант с KribleKrable этих недостатков лишён. А перегрузить класс и переопределить в нём часть функций можно всегда.
                                      • 0
                                        Вариант с KribleKrable ещё не засоряет namespace. Но в нём значительно больше пунктуации, читабельность сомнительна.
                                        • 0
                                          И всё же, если я правильно помню, всевозможные ORM делают именно через классы, безо всяческих syntactic sugar.

                                          Ну, а в варианте с | слишком много переносов строк.
                                    • 0
                                      Это всё, конечно, интересно и красиво, но я бы долго моргал глазами, если бы увидел это в чужом коде.
                                      И я уже молчу про проблемы, описанные Андреем: к чёрту этот блек-джек!
                                      Можно было бы подумать о такой организации настоящих пайпов модуля subprocess, но нужно хорошо подумать…
                                      • 0
                                        Заметной разницы нету, кроме обращения порядка действий. В идеале хотелось бы что-то вроде $ в haskell — порядок остается тем же, а скобки писать не надо
                                        • 0
                                          по-моему, аналогичный эффект можно получить с помощью композиции функций. т.е. «идеалистический» код может выглядеть следующим образом:
                                          wtf = compose([where(lambda x: x % 3 == 1),
                                                         sum,
                                                         my_function])
                                          
                                          result = wtf(x for x in range(100))
                                          

                                          удобно если планируется реиспользовать функцию, хотя немного громоздко для однострочников. «библиотека», в первом приближении:
                                          from functools import partial
                                          
                                          compose2 = lambda f, g: lambda *args, **kws: g(f(*args, **kws))
                                          compose = partial(reduce, compose2)
                                          where = partial(partial, filter) # %) ?
                                          

                                          • +2
                                            Коллеги,

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

                                            Комфортный синтаксис может быть необходимостью только в каком-то более широком контексте: фреймворке, бОльшей билиотеке, для каких-либо специальных задач.

                                            Этот декоратор — proof of concept, не более того. Но красивый, тем и понравился :)
                                            • +3
                                              Хм…

                                              Post.objects | where(user_id = 1) & where(is_published = True) | order('-time_published') | by_page(1, 10)
                                              

                                              Забавно выходит :)
                                              • +2
                                                Вот именно как-то так я и вижу реальное применение подхода. Аккумулировать условия внутри пайпа, вычислять только по прямому требованию.

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

                                                Как насчет потока данных из сети? Скажем, такой синтаксис:

                                                pipeline = socket | read_request | (callback & errback) | send_response
                                                server.register(pipeline)
                                                server.run(80)
                                                


                                                здесь callback — сборка ответа на корректный запрос, errback — на некорректный; а оператор & — объединение корректного и некорректного обработчиков.
                                                • +1
                                                  Ага :) А ещё:

                                                  image | scale(640, 960, crop = True) | rotate(PI/2) | mirrow(...) | invert()
                                                  

                                                  На самом деле у подобного подхода есть любопытное преимущество: такие цепочки можно применять к любым объектам не изменяя их функциональности. Т.е. для image в этом примере не нужно добавлять методы к классу картинки, а scale, rotate, mirrow, invert могут быть универсальными для работы с разными типами.

                                                  И неудобство правда рядом:

                                                  image.scale(640, 960, crop = True).rotate(PI/2).mirrow(...).invert()
                                                  

                                                  Так вполне может быть. Но вот залезать в глобальное пространство имён с функциями типа where, order, invert как-то некрасиво. Скорее, в реальности будет что-то типа:

                                                  from super.image.processor import i
                                                  image | i.scale(640, 960, crop = True) | i.rotate(PI/2) | i.mirrow(...) | i.invert()
                                                  

                                                  А это уже не так элегантно.
                                                • +1
                                                  Поглядите на sqlalchemy. Там забавней — и строже одновременно.
                                                  • 0
                                                    Честно говоря мне не очень нравится sqlalchemy. И Джанговский ORM меня не до конца устраивает. Я вот уже месяц собираюсь написать свою надстройку над Джанговским ORM :)
                                                • +1
                                                  Ваша статья натолкнула меня на некоторые мысли в контексте Ruby.

                                                  В нем проблемы с пайпами как таковой нет:

                                                  my_function(sum(filter(lambda x: x % 3 == 1, [x for x in range(100)])))
                                                  

                                                  записывается как:

                                                  my_function ((1..100) . to_a . keep_if {|x| x % 3 == 1 } . reduce(:+))
                                                  

                                                  Но с генераторами вида:

                                                  def fib
                                                    a, b = 0, 1
                                                    loop do
                                                      yield a
                                                      a, b = b, a + b
                                                    end
                                                  end
                                                  
                                                  fib . take_while {|x| x < 10}
                                                  

                                                  такое уже не проходит.

                                                  Приходится немножко извращаться:

                                                  def fib
                                                    a, b = 0, 1
                                                    res = []
                                                    loop do
                                                      return res unless yield a
                                                      res << a
                                                      a, b = b, a + b
                                                    end
                                                  end
                                                  
                                                  fib {|x| x < 10} # => [0, 1, 1, 2, 3, 5, 8]
                                                  

                                                  Неплохо, но хочется-то предыдущий вариант! :)

                                                  Конечно, можно использовать Fiber, дополнив его методом take_while:

                                                  class Fiber
                                                    def take_while
                                                      res = []
                                                      loop do
                                                        val = self.resume
                                                        return res unless yield val
                                                        res << val
                                                      end
                                                    end
                                                  end
                                                  
                                                  def fib
                                                    Fiber.new do
                                                      a, b = 0, 1
                                                      loop do
                                                        Fiber.yield a
                                                        a, b = b, a + b
                                                      end
                                                    end
                                                  end
                                                  
                                                  fib . take_while {|x| x < 10} # => [0, 1, 1, 2, 3, 5, 8]
                                                  

                                                  Работает. Но что же это получается: каждую функцию, от которой требуется свойство бесконечного генератора, внутри оборачивать в Fiber? Noway! Я слишом ленив для такого.

                                                  И тут мой взор упал на декораторы питона: «Хорошая штука» — подумал я, сейчас возьму такую же в любимом руби и… внезапно в Ruby такого не оказалось.
                                                  «Вещь то полезная… надо реализовать!» — подумал небезызвестный Yehuda Katz и забацал поддержку декораторов для руби в 80 строчек кода.

                                                  Пишем свой декоратор:

                                                  class Generator < Decorator
                                                    def call(this, *args)
                                                      Fiber.new { loop { @method.bind(this).call(*args) { |x| Fiber.yield x } } }
                                                    end
                                                  end
                                                  

                                                  и класс для бесконечного генератора чисел фибоначчи с «чистым» методом fib:

                                                  class FibGen
                                                    extend MethodDecorators
                                                  
                                                    Generator()
                                                    def fib
                                                      a, b = 0, 1
                                                      loop do
                                                        yield a
                                                        a, b = b, a + b
                                                      end
                                                    end
                                                  end
                                                  
                                                  FibGen.new.fib . take_while {|x| x < 10} # => [0, 1, 1, 2, 3, 5, 8]
                                                  FibGen.new.fib . take(7) . map {|x| x * 2} . reduce(:+) # => 40
                                                  

                                                  Отлично! :)

                                                  Итого: добрая половина дня убита занятием вот этой замечательной фигней ^.^
                                                  • +2
                                                    Вижу, разгорелся небольшой спор о том, что такое pythonic и что — нет.
                                                    Четкого определения нет и быть не может.
                                                    Мое чутье основывается на многолетнем чтении рабочей переписки разработчиков питона.
                                                    И на собственном опыте, конечно.
                                                    Иногда вроде бы красивая идея отвергается по результатам обсуждения.
                                                    Причины бывают разные.
                                                    Например, предлагаемый подход работает не в том духе, в котором сделано все остальное. Это может вводить пользователя в недоумение. В малых дозах — не страшно. В больших — становится натуральным ядом.
                                                    Случается и другое: работает в большинстве очевидных случаев, но в «темных углах» поведение непредсказуемо. Более того, правильное поведение неочевидно. Или мнения делятся «пополам-на-пополам».
                                                    Значит — решение плохое. Его можно применять в сторонних библиотеках — но в стандарт оно не войдет.
                                                    Бывает и третье. Все хорошо и понятно. Во мнениях сошлись. Но генерируемые ошибки (логичные и понятные) — неочевидны. Хороший спец понимает их с полуслова. «Простой программист» теряется в догадках. Это — тоже весомый минус. Предложение не будет принято.
                                                    Иногда ван Россум говорит: «Жопой чую, что-то здесь не так. Давайте отложим решение до лучших времен. И еще раз подумаем».

                                                    Это — строгие критерии отбора в core development team. Сторонние библиотеки не обязаны им следовать.

                                                    Более того. Уверен, что я перечислил далеко не все случаи — просто вспомнил о наиболее очевидных.

                                                    Что такое pythonic или unpythonic каждый решает сам для себя. И, тем не менее, я стараюсь следовать «чувству правильного», которое крайне неочевидно. Ориентир — тот дух, что пропитывает решения создателей языка.
                                                    Не одна моя поделка при внимательном рассмотрении оказалась — unpythonic. Красивая и лаконичная запись — и затем море проблем. Что-то улучшалось, гораздо большее — выбрасывалось. Жаль, но так лучше…
                                                    • +1
                                                      Брутальный образ Гвидо вырисовывается — мол, жопой чую %)
                                                      Предлагаю первую букву BDFL развёртывать в brutal.
                                                    • 0
                                                      Интересный подход. Но такие вещи все же надо делать на уровне языка, тогда это было бы вообще замечательно, а так это усложняет отладку кода и понимание кода всеми, кроме его автора. Переопределение операторов все же зло в плане читабельности кода, так как непонятно с чем ты имеешь дело, с переопределенным оператором или нативным.
                                                      • 0
                                                        В сочетании с ipython это просто шикарно для использования в качестве оболочки командной строки.
                                                        • 0
                                                          Забрел тут в обсуждение такого рода пайпов пару месяцев случайно… А ведь действительно удобней получится. В живых интерпретаторах с CLI это действительно удобней и наглядней. Попробовать, что ли, написать что-нибудь..?

                                                          Обработку картинок?
                                                        • 0
                                                          Ох, если нужно писать понятно, то лучше уж писать «четверостишиями» действительно длинные связки, тогда названиями переменных можно одновременно и код документировать.

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