-
-
Notifications
You must be signed in to change notification settings - Fork 32.9k
Description
Feature or enhancement
Proposal:
See discourse for a complete proposal.
This is one of basic components for functional programming.
It would be useful as it in various cases, such as predicate composition.
Also the user could inherit from it to implement operator-based pipeline syntax, which is not much worse than some of the proposals for specialised full-fledged pipeline statements.
So I propose a minimal pipe
implementation.
class pipe:
def __init__(self, *funcs):
self.funcs = funcs
def __call__(self, obj, /, *args, **kwds):
if not (funcs := self.funcs):
assert not args and not kwds
return obj
first, *rest = funcs
if args or kwds:
obj = first(obj, *args, **kwds)
else:
obj = first(obj)
for func in rest:
obj = func(obj)
return obj
def __get__(self, obj, objtype=None):
if obj is None:
return self
return types.MethodType(self, obj)
Features:
pipe
supports full signature of the first function.- Having
__get__
it behaves in line withdef
andpartial
. (see examples below) - Behaves as identity function without input functions
There are other possible additions to it, but user can implement most of those in inherited classes.
Implementation side is straight forward (including inspect.signature
) so the only question is whether it is desirable to have this.
Other languages that have it (or considered it):
It is the same concept as in JavaScript, but differs from Rust and C++ proposal in a way that it does not intend to provide a full pipelining framework, but rather simple utility function that can be modularly combined with other tools.
E.g. Rust example can be achieved by implementing independent functions that do parallel work:
from functools import partial
class pipe(functools.pipe):
"""Convenience subclass with operator overloading"""
__ror__ = functools.__call__
def __rshift__(self, func):
return self.__class__(*self.funcs, *(funcs,))
# Build the first 10 fibonacci numbers in parallel,
# then double them:
parallel_map = map # Not parallel :)
def fibonacci(n):
return 1 if n<2 else fibonacci(n-1) + fibonacci(n-2)
result = range(10) | (pipe()
>> partial(parallel_map, fibonacci)
>> partial(map, lambda x: x*x)
>> list
)
print(result) # [1, 1, 4, 9, 25, 64, 169, 441, 1156, 3025]
# Input not being tied to `pipe` object (as it is in Rust)
# allows re-using of pipelines for different inputs:
pipeline = pipe(
partial(parallel_map, fibonacci),
partial(map, lambda x: x*x),
list
)
pipeline([20, 40]) # [119814916, 27416783093579881]
pipeline(range(14, 16)) # [372100, 974169]
Couple of additional benefits:
A) Convenience for pipe-like method modifications.
class A:
a = 1
def factorial_plus(self, n):
return math.factorial(n + self.a)
neg_factorial_plus = pipe(factorial_plus, operator.neg)
B) pipe()
without arguments acts as identity function.
Personally, I would use it instead of defining my own lambda
as it would most likely be more performant and also I would like to rely on one implementation for all identity function needs.
The way I see it, the benefits could potentially outweigh the costs:
- Benefits: multi-purpose utility function with different applications, which is fun to play with.
- Costs: It would be the least complex object in
functools
. Implementation is straight forward without any foreseeable obstacles and there is nothing new to invent here.
Has this already been discussed elsewhere?
I have already discussed this feature proposal on Discourse