Pipe: Infix syntax for Python

Posté le mar. 04 avril 2017 dans blog

Pipe is a Python module enabling infix syntax in Python. For those asking "Why ?" let's take an example: Compare the readability of the classical prefix syntax:

sum(select(where(take_while(fib(), lambda x: x < 1000000) lambda x: x % 2), lambda x: x * x))

And the infix syntax:

fib() | take_while(lambda x: x < 1000000) \
      | where(lambda x: x % 2) \
      | select(lambda x: x * x) \
      | sum()

Isn't the infix syntax more readable? The base class of Pipe is kept simple (7 lines of python) and is usable as a decorator permitting you to create new 'pipeable' functions easily. The module provides like 30 prepared pipes functions like where, group_by, sort, take_while... A pipeable function takes an iterable (tuple, list, generator) and yields to be itself an iterator, so pipeable function can be piped together. Let me introduce the basic usage of the Pipe module, then I'll write some bits on how to build new ones: To start, get it from PyPI http://pypi.python.org/pypi/pipe/1.3 and install it, open a REPL, import pipe, and play:

Python 2.6.6 (r266:84292, Dec 26 2010, 22:31:48)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pipe import *
>>> [1, 2, 3, 4, 5] | add
15
>>> [5, 4, 3, 2, 1] | sort
[1, 2, 3, 4, 5]

Until here it's easy, to know more about available pipes, just read the help(pipe) in the REPL, all are explained with an example as a doctest Now as we know that pipeable functions use iterables, we can try to pipe together two or more pipeables:

>>> [1, 2, 3, 4, 5] | where(lambda x: x % 2) | concat
'1, 3, 5'
>>> [1, 2, 3, 4, 5] | where(lambda x: x % 2) | tail(2) | concat
'3, 5'
>>> [1, 2, 3, 4, 5] | where(lambda x: x % 2) | tail(2) | select(lambda x: x * x) | concat
'9, 25'
>>> [1, 2, 3, 4, 5] | where(lambda x: x % 2) | tail(2) | select(lambda x: x * x) | add
34

Now, a bit about lazyness, as Pipe use iterables, the evaluation of a whole Pipe is lazy, so we can play with infinite generators like this one :

>>> def fib():
...    a, b = 0, 1
...    while True:
...        yield a
...        a, b = b, a + b

Now we can do every kind of stuff into the fibonacci sequence, like solving the 2nd problem of http://projecteuler.net in a readable one liner:

Find the sum of all the even-valued terms in Fibonacci which do not exceed four million.

>>> euler2 = fib() | where(lambda x: x % 2 == 0) | take_while(lambda x: x < 4000000) | add
>>> assert euler2 == 4613732

Isn't it pretty? Let now see how to create new pipeable functions using the @pipe decorator: You want to create a function that yields the first x elements from its input You want its usage to be (1, 2, 3, 4, 5) | take(2) to take the fist 2 elements. I know that you are thinking about a basic implementation like:

def take(iterable, qte):
    for item in iterable:
        if qte > 0:
            qte -= 1
            yield item
        else:
            return

Right? You take an iterable, a qantity, and while the quantity is not reached, you just have to yield? OK, just add @pipe to you take function and it's pipeable :-)