Одни питонисты любят код читаемый, другие предпочитают лаконичный. К сожалению, баланс между первым и вторым — решения по-настоящему изящные — редко случается встретить на практике. Чаще стречаются строки вроде
Не в Питоне?
Простую реализацию подобных цепочек не так давно предложил некий Julien Palard в своей библиотеке Pipe.
Начнем сразу с примера:
Упс, интуитивный порыв не прокатил. Пайп возвращает генератор, значения из которого еще только предстоит извлечь.
Можно было бы вытащить значения из генератора встроенной функцией приведения типа list(), но автор инструмента был последователен в своих изысканиях и предложил нам функцию-пайп as_list.
Как видим, источником данных для пайпов в примере стал простой список. Вообще же говоря, использовать можно любые итерируемые (iterable) сущности Питона. Скажем, «пары» (tuples) или, что уже интересней, те же генераторы:
Разумеется, радость была бы неполной, не будь у нас легкой возможностисоздавать собственные пайпы. Пример:
Честно говоря, удивительно было увидеть, насколько лаконичен базовый код модуля! Судите сами:
В конструкторе декоратор сохраняет декорируемую функцию, превращая ее в объект класса Pipe.
Если пайп вызывается методом __call__ — возвращается новый пайп функции с заданными аргументами.
Главная тонкость — метод __ror__. Это инвертированный оператор, аналог оператора «или» (__or__), который вызывается у правого операнда с левым операндом в качестве аргумента.
Получается, что вычисление цепочки начинается слева направо. Первый элемент передается в качестве аргумента второму; результат вычисления второго — третьему и так далее. Безболезненно проходят по цепочке и генераторы.
На мой взгляд, очень и очень элегантно.
Синтаксис у такого рода пайпов действительно простой и удобный, хотелось бы увидеть что-то подобное в популярных фреймворках; скажем, для обработки потоков данных; или — в декларативной форме — выстраивания в цепочки коллбеков.
Единственным недостатком именно реализации являются довольно туманные трейсы ошибок.
О развитии идеи и альтернативных реализация — в следующих статьях.
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
Отсюда можно извлечь несколько уроков:- в пайпах можно использовать списки, «пары», генераторы — любые iterables.
- результатом объединения генераторов в цепочки станет генератор.
- без явного требования (приведения типа или же специального пайпа) пайпинг является «ленивым» в том смысле, что цепочка есть генератор и может служить бесконечным источником данных.
Разумеется, радость была бы неполной, не будь у нас легкой возможностисоздавать собственные пайпы. Пример:
@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__), который вызывается у правого операнда с левым операндом в качестве аргумента.
Получается, что вычисление цепочки начинается слева направо. Первый элемент передается в качестве аргумента второму; результат вычисления второго — третьему и так далее. Безболезненно проходят по цепочке и генераторы.
На мой взгляд, очень и очень элегантно.
Послесловие
Синтаксис у такого рода пайпов действительно простой и удобный, хотелось бы увидеть что-то подобное в популярных фреймворках; скажем, для обработки потоков данных; или — в декларативной форме — выстраивания в цепочки коллбеков.
Единственным недостатком именно реализации являются довольно туманные трейсы ошибок.
О развитии идеи и альтернативных реализация — в следующих статьях.