Skip to main content

Recursive Pipes for Python

Project description

Recursive Pipes for Python

Pipes are a common pattern in functional programming. It mainly boils down to having some kind of input, which is provided to a chain of methods which successively take the input, process it and provide the output to the next method in the chain, which takes it as input and so forth.

In functional programming of course each method should be pure and only accept the output from the previous method as input. In this python implementation of couse You are free to deviate from this restriction, gaining all the benefits - and possible pitfals. But this is not an introduction to functional programming.

There are already (good) implementations for pipes in python, e.g. JulienPalard/Pipe, which one You choose, if at all, is a bit up to your liking. This implementation aims to be flexible, easily extendable and allows for easy recursion into substructures within your input. It also uses the dot-notation and not the pipe-notation (|).

Installation

TODO

Usage and Examples

Intro

Pipes in their simplest form can be used like that:

from recursive_pipes import Pipe
# ... other imports

pipe = Pipe[Iterable[int], Iterable[int]](range(10))

@pipe.append
def onlyOdd(iterable):
    return itertools.filterfalse(lambda num: num % 2 == 0, iterable)

@pipe.append
def firstNumberBiggerThan4(iterable):
    return itertools.dropwhile(lambda num: num <= 4, iterable)

print(list(pipe.exec()))
# [5, 7, 9]

This is not very useful yet, although it shows some basic principles. Note, that the Typehints are not needet, but it helps the Type-Checker to check against input and output type of the pipe (here both are Iterable[int]).

Extending the standard Pipe

To have this become a bit more useful, You can subclass the Pipe-class and add some pipe functions. By default only a very limited set of functions exist, namely append (we have seen that already) and the special functions recurseIntoDictValues and recurseIntoIterable. - Also You should not overwrite the other methods from the Pipe-class without knowing what You do ;).

To have some more tools at hand, we first need to extend the Pipe class:

Input = TypeVar('Input')
Output = TypeVar('Output')

class IterPipe(Pipe[Input, Output]):
    def filterfalse(self, callback: Callable[[Any], bool]):
        def filterfalse(iterable):
            return itertools.filterfalse(callback, iterable)
        return self.append(filterfalse)

    def filter(self, callback: Callable[[Any], bool]):
        # a bit more concise using partial
        return self.append(partial(filter, callback))

    # if additional parameters (here the initializer argument for reduce) shall
    # be specified, an additional wrapper function must be implemented
    def reduce(self, initializer=object()):
        def reduceByCallback(callback: Callable):
            # a hack to determine, if the initializer argument was passed by
            # the user or is just the default value
            if initializer is self.reduce.__defaults__[0]:
                self.append(lambda iterable: reduce(callback, iterable))
            else:
                self.append(lambda iterable: reduce(callback, iterable, initializer))
            return self
        return reduceByCallback

Remark the Generic Input and Output parameters, which will enable You later to specify type-hints for your input and output type.

Actually the IterPipe class already exists in the recursive_pipes-package, so You can import it and use or mix it into your own Pipe-classes as You see fit.

Different Styles for creating the Pipe

This class can now be used instead of the default Pipe class:

@(pipe := IterPipe[Iterable[int], int](range(20))).filter
def onlyOdd(num):
    return num % 2 == 0

@pipe.reduce(10)
def sum(carry, x):
    return carry + x

print(pipe.exec())
# 100

Alternatively, You can call it like that:

result = ((IterPipe(range(20))).filter(lambda num: num % 2 == 0)
                               .reduce(10)(lambda carry, x: carry + x)
                               .exec())
print(result)
# 100

Mixed Variants are possible:

pipe = IterPipe()

@pipe.filter
def onlyOdd(num):
    return num % 2 == 0

print(pipe.reduce()(lambda carry, x: carry + x).exec(range(20)))
# 90

Reusing Pipes / Continuing Usage after Execution

By default Pipes can be reused as often as You like to after their definition.

pipe = IterPipe(range(20))
pipe.filter(lambda num: num % 2 == 0)
print(list(pipe.exec()))
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
print(list(pipe.exec(10)))
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

When using the special memorize-Parameter to the exec-call. The result is stored in the pipe and will be reused as input in subsequent calls (otherwise the input is always the input lastly specified).

pipe = IterPipe(range(20))
pipe.filter(lambda num: num % 2 == 0)
# memorize the result as input for subsequent executions
result = pipe.append(lambda x: list(x)).exec(range(10), memorize=True)
print(result)
# [0, 2, 4, 6, 8]
print(pipe.reduce()(lambda carry, x: carry + x).exec())
# 20

The pipe can be reset, meaning, that the all existing callbacks will be removed.

pipe = IterPipe(range(10))
pipe.filter(lambda num: num % 2 == 0)
print(pipe.exec())
# [0, 2, 4, 6, 8]
pipe.reset()
print(pipe.exec())
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Advanced Features: Recursing into Dictionaries / Iterables

If you have nested dictionaries / lists, you can recurse into them, which will automatically create a subpipe on these lists / dictionaries.

from collections import Counter

itemsInStore = { 'storehouse1': ['apple', 'pear', 'pear'],
                 'storehouse2': ['mushroom', 'mushroom', 'mario'] }

with IterPipe[dict[str, list[str]], dict](itemsInStore) as pipe:
    @pipe.recurseIntoDictValues()
    def countByName(subpipe: IterPipe):
        @subpipe.append
        def countByName(items: Iterable[str]):
            return Counter(items)

    pprint.pprint(dict(pipe.exec()))
# {'storehouse1': Counter({'pear': 2, 'apple': 1}),
#  'storehouse2': Counter({'mushroom': 2, 'mario': 1})}

This can be done arbitrarily often.

tinyWorlds = [ {'name': 'snowflake', 'inhabitants': {2019: 22, 2020: 50, 2021: 49}},
               {'name': 'pythonplanet', 'inhabitants': {2018: 44, 2022: 100}} ]

pipe = IterPipe(tinyWorlds)

@pipe.recurseIntoIterable
def mapWorld(subpipe: IterPipe):
    @subpipe.append
    def copyKey(world: dict):
        world['avgInhabitants'] = world['inhabitants']
        return world

    @subpipe.recurseIntoDictValues(withKey=True)
    def mapInhabitants(subsubpipe: IterPipe):
        @subsubpipe.append
        def averageInhabitants(key, value):
            if key == 'avgInhabitants':
                return sum(value.values())/len(value.values())
            return value

    @subpipe.append
    def printWorld(world: dict):
        print(f"Name: {world['name']}")
        print(f"Average Inhabitants: {world['avgInhabitants']}")

pipe.exec()
# Name: snowflake
# Average Inhabitants: 40.333333333333336
# Name: pythonplanet
# Average Inhabitants: 72.0

Developement

Testing

Clone the project and run pytest from project root (the directory containing the pyproject.toml file).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

recursive_pipes-0.0.1.tar.gz (6.4 kB view details)

Uploaded Source

Built Distribution

recursive_pipes-0.0.1-py3-none-any.whl (7.0 kB view details)

Uploaded Python 3

File details

Details for the file recursive_pipes-0.0.1.tar.gz.

File metadata

  • Download URL: recursive_pipes-0.0.1.tar.gz
  • Upload date:
  • Size: 6.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.0.1.dev0+g94f810c.d20240510 CPython/3.12.3

File hashes

Hashes for recursive_pipes-0.0.1.tar.gz
Algorithm Hash digest
SHA256 2649e9d8be031e89354f489ccc192b4aba7a154a67677841561d85bcc4c26622
MD5 2e4062f02888f75dfee49cc2f10dbdae
BLAKE2b-256 734cafbf91c59aa6846a86dabc5d853df25732cd008de22c3c6b7426964ce4eb

See more details on using hashes here.

File details

Details for the file recursive_pipes-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: recursive_pipes-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 7.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.0.1.dev0+g94f810c.d20240510 CPython/3.12.3

File hashes

Hashes for recursive_pipes-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 7116f281bf25bbfd6dbb1748a7fa2cba888ffa27e0e0e1cc8b66d137eab70ad4
MD5 72e79a1077a88d7e42a9160cb52f3414
BLAKE2b-256 5aa83e35c85fc0dcdba8e96d2cc91fe545c1099c541fc0033879bab6ae5d2217

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page