pipe

repository·main·Indexed 24 days ago

https://github.com/julienpalard/pipe

An infix programming toolkit for Python that enables a shell-like syntax using the `|` operator. It allows developers to build expressive, lazy-evaluated data processing pipelines using the `Pipe` class and `@Pipe` decorator to chain operations on iterables.

Tokens
2.1K
Snippets
7
Records
9
Agent score
35%

What's inside pipe

  1. How Pipe infix syntax works

    main

    Pipe enables a shell-like infix syntax in Python using the | operator. This allows you to chain operations on iterables in a readable, linear fashion.

    Each pipe is lazily evaluated, can be aliased (pre-configured with arguments), and can be partially initialized. This makes it ideal for building complex data processing pipelines.

    Example of aliasing a pipe:

    is_even = where(lambda x: x % 2 == 0)
    sum(fib() | is_even | take_while(lambda x: x < 4000000))
    sum(fib() | where(lambda x: x % 2 == 0) | take_while(lambda x: x < 4000000))
  2. How lazy evaluation works in Pipe

    main

    Pipe is built on generators, making it naturally lazy. Defining a pipe chain does not trigger any computation or data generation; values are only processed when a consumer (like list(), sum(), or a for loop) pulls them through the chain.

    This is particularly useful when working with infinite generators like itertools.count(). The pipeline will only execute as many steps as necessary to satisfy the final consumer.

    Example of lazy execution:

    from itertools import count
    
    # This chain defines the logic but executes nothing
    result = (count()
              | tee
              | select(lambda x: x ** 2)
              | take(10)
              | where(lambda x: x % 2))
    
    # Computation only starts here when we pull the first value
    print(list(result | take(1)))
    from itertools import count
    
    result = (count()
              | tee
              | select(lambda x: x ** 2)
              | take(10)
              | where(lambda x: x % 2))
    
    print(list(result | take(1)))
  3. Install the Pipe library

    main

    To install the pipe library, use pip via the command line. Use the specific command for your operating system:

    Linux/macOS:

    python3 -m pip install pipe

    Windows:

    py -3 -m pip install pipe
    # Linux/macOS
    python3 -m pip install pipe
    
    # Windows
    py -3 -m pip install pipe
  4. Migrating from Pipe 1.x to 2.x

    main

    In Pipe 2.0, all 'closing pipes' (pipes that return non-iterables, such as add or as_list) have been removed to ensure all pipes return iterables and maintain consistency.

    If you are upgrading from 1.x and encounter exceptions, use one of these three strategies:

    1. Replace closing pipes with standard Python functions: Instead of ... | as_list, use list(...). Instead of ... | add, use sum(...).
    2. Reimplement necessary closing pipes: Since @Pipe makes it easy to create custom logic, you can implement the specific closing pipes you need in a few lines of code.
    3. Pin the version: If you cannot refactor immediately, install the older version: pip install pipe<2.

    Tip: Use Python's Development Mode to catch deprecation warnings before they become breaking changes.

  5. Use `tee` for debugging pipe stages

    main

    The tee pipe is useful for inspecting the state of a pipeline at any given stage. It prints the current items to standard output and then yields them unchanged to the next stage in the pipe.

    from pipe import tee, map
    
    # This will print '1', '2', '3', '4', '5' to stdout during execution
    sum(["1", "2", "3", "4", "5"] | tee | map(int) | tee)
    from pipe import tee
    sum(["1", "2", "3", "4", "5"] | tee | map(int) | tee)
  6. Create custom pipes using the @Pipe decorator

    main

    You can define your own infix operations by decorating a generator function with @Pipe. A decorated function must accept an iterable as its first argument and yield values. This allows you to use your custom function within a pipe chain using the | operator.

    For multi-argument pipes, you can partially initialize them (currying) to create specialized pipes. This works with both positional and keyword arguments.

    Example of a custom pipe with partial initialization:

    @Pipe
    def addmul(iterable, to_add, to_mul):
        """Computes (x + to_add) * to_mul to every items of the input."""
        for i in iterable:
            yield (i + to_add) * to_mul
    
    # Partially initialize with to_add=0
    add_zero = addmul(0)
    # Use the specialized pipe
    list(range(10) | add_zero(10))
    @Pipe
    def addmul(iterable, to_add, to_mul):
        """Computes (x + to_add) * to_mul to every items of the input."""
        for i in iterable:
            yield (i + to_add) * to_mul
    
    add_zero = addmul(0)
    list(range(10) | add_zero(10))
  7. Construct custom pipes using the Pipe class

    main

    You can create your own pipes by instantiating the Pipe class. The class takes a function that accepts an iterable as its first argument.

    Basic Pipe

    from pipe import Pipe
    square = Pipe(lambda iterable: (x ** 2 for x in iterable))

    Wrapping existing functions

    If a function already takes an iterable as its first argument, wrapping it is straightforward:

    from collections import deque
    from pipe import Pipe
    end = Pipe(deque)
    # Usage: list(range(10) | end(3))

    One-off pipes with arguments

    You can specify positional and named arguments directly when creating a Pipe instance:

    from itertools import combinations
    list(range(5) | Pipe(combinations, 2))
  8. Create pipeable functions with the @Pipe decorator

    main

    The @Pipe decorator can be applied to functions or methods to make them compatible with the | operator. The decorated function must take an iterable as its first argument.

    Decorating a function

    @Pipe
    def running_average(iterable, width):
        items = deque(maxlen=width)
        for item in iterable:
            items.append(item)
            yield mean(items)
    
    list(range(20) | running_average(width=2))

    Decorating class methods

    You can use @Pipe with instance methods, class methods, or static methods to organize logic within classes:

    Instance Method:

    class Factor:
        def __init__(self, n: int):
            self.n = n
        @Pipe
        def mul(self, iterable):
            return (x * self.n for x in iterable)
    
    fact = Factor(10)
    list([1, 2, 3] | fact.mul)

    Class Method:

    class Factor:
        n: int = 10
        @Pipe
        @classmethod
        def mul(cls, iterable):
            return (x * cls.n for x in iterable)
    
    list([1, 2, 3] | Factor.mul)

    Static Method:

    class Factor:
        @Pipe
        @staticmethod
        def mul(iterable):
            return (x * 10 for x in iterable)
    
    list([1, 2, 3] | Factor.mul)
    @Pipe
    def running_average(iterable, width):
        items = deque(maxlen=width)
        for item in iterable:
            items.append(item)
            yield mean(items)
  9. Reference: Available Pipe functions

    main

    The following functions are available as pipes in the pipe module. Many are aliases or wrappers around itertools functions.

    PipeDescription
    batched(n)Like itertools.batched; yields chunks of size n
    chainUnfolds an iterable containing ONLY iterables
    chain_with(other)Yields elements of the given iterable, then elements of other
    dedup(key=None)Deduplicates values; optional key function
    enumerate(start=0)Like built-in enumerate()
    filter(predicate)Alias for where(predicate)
    groupby(key=None)Like itertools.groupby on a sorted iterable
    islice(start, stop, step)Like itertools.islice
    izip(*iterables)Like itertools.izip
    map(fct) / select(fct)Applies fct to each element; select is an alias for map
    netcat(host, port)Sends/receives bytes over TCP
    permutations(r=None)Returns all possible permutations
    reverseLike built-in reversed()
    skip(n)Skips the first n elements
    skip_while(predicate)Like itertools.dropwhile
    sort(key=None, reverse=False)Like built-in sorted()
    t(val)Like Haskell's : operator; chains values into a sequence
    tail(n)Yields the last n elements
    take(n)Yields the first n elements
    take_while(predicate)Like itertools.takewhile
    teeOutputs to stdout and yields unchanged items (useful for debugging)
    transpose()Transposes rows and columns of a matrix
    traverseRecursively unfolds nested iterables
    uniq(key=None)Deduplicates only consecutive values
    where(predicate) / filter(predicate)Yields only items matching the predicate