18 апреля 2011 в 02:19

Пайпы, 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__), который вызывается у правого операнда с левым операндом в качестве аргумента.

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

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

Послесловие


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

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

О развитии идеи и альтернативных реализация — в следующих статьях.
Владимир Казанов @VlK
карма
204,6
рейтинг 0,0
Пользователь
Самое читаемое Разработка

Комментарии (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
    Ох, если нужно писать понятно, то лучше уж писать «четверостишиями» действительно длинные связки, тогда названиями переменных можно одновременно и код документировать.

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