PyFunctional Documentation

repository·master·Indexed 25 days ago

https://github.com/entilzha/pyfunctional

A Python library for creating data pipelines using chained functional operators. It provides an expressive API inspired by Scala, Spark, and LINQ, supporting lazy execution, parallel processing via pseq, and integration with various data sources including JSON, CSV, and SQLite3. The library categorizes operations into Streams (entry points), Transformations (lazy modifiers like map and filter), and Actions (terminal evaluations like reduce and sum).

Tokens
8.4K
Snippets
12
Records
20
Agent score
82%

What's inside PyFunctional

  1. Apply Transformations and Actions to streams

    master
    Once a stream is initialized, you can manipulate the data using the Transformations and Actions API. Transformations modify the stream (e.g., mapping, filtering) and return a new stream, while Actions perform computations on the stream (e.g., reducing, counting) and typically return a concrete value.
  2. How streams, transformations, and actions work together

    master

    PyFunctional operations are categorized into three types that form a data pipeline:

    1. Streams: The entry point that reads data for use by the collections API (e.g., seq(data) or seq.json('file.json')).
    2. Transformations: Functions that transform data within a stream without evaluating it immediately (e.g., map, flat_map, filter, where, select).
    3. Actions: Functions that trigger the evaluation of the transformation chain to produce a concrete value (e.g., to_list, reduce, to_dict, sum, max_by).

    Example pipeline: seq(1, 2, 3).map(lambda x: x * 2).reduce(lambda x, y: x + y)

    • seq is the stream
    • map is the transformation
    • reduce is the action.
    seq(1, 2, 3).map(lambda x: x * 2).reduce(lambda x, y: x + y)
  3. How lazy execution and lineage work

    master

    PyFunctional uses lazy execution by tracking the list of transformations applied to a sequence (called its lineage). Computations are only evaluated when an action is called.

    This lineage tracking allows for caching results to prevent expensive re-computation. For example, calling size() or repr() will cache the underlying sequence so that subsequent operations don'|n't re-run the entire pipeline.

    To inspect the current lineage, you can access the _lineage attribute.

    def times_2(x):
        return 2 * x
    
    elements = (
       seq(1, 1, 2, 3, 4)
          .map(times_2)
          .peek(print)
          .distinct()
    )
    
    elements._lineage
    # Lineage: sequence -> map(times_2) -> peek(print) -> distinct
    
    l_elements = elements.to_list()
    # Prints: 1
    # Prints: 1
    # Prints: 2
    # Prints: 3
    # Prints: 4
    
    elements._lineage
    # Lineage: sequence -> map(times_2) -> peek(print) -> distinct -> cache
    
    l_elements = elements.to_list()
    # The cached result is returned so times_2 is not called and nothing is printed
  4. Initialize a stream with seq()

    master
    To start using PyFunctional, import the seq function from functional. The seq function returns an instance of functional.streams.Stream, which serves as the entry point for all data processing. You can use seq to wrap existing Python collections or use specialized stream methods like .csv() to read data from files.
  5. Enable Parallel Execution with pseq

    master

    To parallelize "embarrassingly parallel" operations, import pseq instead of seq. This uses Python's multiprocessing to run operations in parallel.

    Supported parallel operations:

    • map / select
    • filter / filter_not / where
    • flat_map

    PyFunctional automatically squashes chains of these operations to reduce multiprocessing overhead.

  6. Write to compressed files

    master

    When using to_ functions to write files, you can specify compression. PyFunctional supports gzip (or gz), lzma (or xz), and bz2.

    Set the compression parameter to one of these values. If not set, it defaults to None (no compression).

  7. Use Scala/Spark or LINQ inspired APIs

    master

    PyFunctional provides multiple ways to express the same logic, allowing you to choose the syntax that fits your background:

    • Scala/Spark style: Uses filter and map.
    • LINQ style: Uses where and select.
    • fn style: Uses the fn library's underscore _ syntax for more concise lambdas.

    Example filtering transactions:

    from functional import seq
    from collections import namedtuple
    from fn import _
    
    Transaction = namedtuple('Transaction', 'reason amount')
    transactions = [
        Transaction('github', 7),
        Transaction('food', 10),
        Transaction('coffee', 5),
        # ... other transactions
    ]
    
    # Scala/Spark inspired
    food_cost = seq(transactions)\n    .filter(lambda x: x.reason == 'food')\n    .map(lambda x: x.amount).sum()
    
    # LINQ inspired
    food_cost = seq(transactions)\n    .where(lambda x: x.reason == 'food')\n    .select(lambda x: x.amount).sum()
    
    # Using fn underscore syntax
    food_cost = seq(transactions).filter(_.reason == 'food').map(_.amount).sum()
  8. Perform Aggregates and Joins

    master

    PyFunctional excels at complex data manipulations like grouping, reducing by key, and joining different data sources.

    • reduce_by_key: Useful for word counts or frequency analysis.
    • group_by: Groups elements by a key.
    • inner_join: Joins two streams based on common keys.
    # Word count example
    words = 'I dont want to believe I want to know'.split(' ')
    seq(words).map(lambda word: (word, 1)).reduce_by_key(lambda x, y: x + y)
    
    # Joining JSON users with JSONL messages
    users = seq.json('examples/users.json')
    messages = seq.jsonl('examples/chat_logs.jsonl')
    
    message_tuples = messages.group_by(lambda m: m['user'])
    data = users.inner_join(message_tuples)
  9. Use the `no_wrap` option to prevent Sequence wrapping

    master

    By default, functions like first() or last() wrap their result in a Sequence if the returned element is itself an iterable. To get the raw element instead, use the no_wrap=True option.

    This option can be passed to:

    • first(no_wrap=True)
    • last(no_wrap=True)
    • head(no_wrap=True)
    • head_option(no_wrap=True)
    • last_option(no_wrap=True)
    • seq(..., no_wrap=True)
    • The Sequence() constructor
    >>> s = seq(list(), list())
    >>> type(s.first())
    <class 'functional.pipeline.Sequence'>
    
    >>> type(s.first(no_wrap=True))
    <class 'list'>
    
    >>> type(seq([list(), list()], no_wrap=True).last())
    <class 'list'>