A library supporting functional programming in Python.
Tested environments
Tested on Python 3.6.3
.
Installation
Install with pip as follows:
pip install dpl
func_pipe
decorator
This decorator turns a function or an object method into a func_pipe
object and then you can pass arguments to it in different ways which are convenient to build pipelines later.
Let's see an example. Assume that you have the following functions decorated with func_pipe
:
from dpl.pipe import func_pipe
@func_pipe
def add_1(x):
return x + 1
@func_pipe
def multiply_2(x):
return x * 2
@func_pipe
def subtract_3(x):
return x - 3
Then you can write a pipeline like this:
y = 1 | add_1 | multiply_2 | subtract_3
which is equivalent to:
y = subtract_3(
multiply_2(
add_1(1)))
Note that a func_pipe
object is callable, so you can use it as a normal function if you need as the above code.
Plain argument passing
The first way to pass an argument to a func_pipe
object is to use the operator |
as you see above:
from dpl.pipe import func_pipe
@func_pipe
def sum_all(array):
return sum(array)
y = (1, 2, 3) | sum_all
which is equivalent to:
def sum_all(array):
return sum(array)
y = sum_all((1, 2, 3))
Unpacking argument passing
Unpacking a tuple of arguments
You can pass a tuple of arguments and unpack them when passing to a func_pipe
object by using the operator >>
. For example:
@func_pipe
def my_sum(x, y, z):
return x + y + z
y = (1, 2, 3) >> my_sum
is equivalent to:
def my_sum(x, y, z):
return x + y + z
y = my_sum(1, 2, 3)
Unpacking a dictionary of arguments
You can also pass a dictionary of arguments and unpack them when passing to a func_pipe
object by using the operator >>
. For example:
from dpl.pipe import func_pipe
@func_pipe
def my_sum(x, y, z):
return x + y + z
y = {'x': 1, 'y': 2, 'z': 3} >> my_sum
is equivalent to:
def my_sum(x, y, z):
return x + y + z
y = my_sum(x=1, y=2, z=3)
numpy.ndarray
, pandas.DataFrame
objects and other objects having definitions for operators |
or >>
Passing Because numpy.ndarray
and pandas.DataFrame
classes define the |
and >>
operators, so to pass their objects to a func_pipe
object, you need to wrap
them in a tuple or a list and passing with unpacking operator >>
. For example:
import numpy as np
from dpl.pipe import func_pipe
@func_pipe
def my_sum(array):
return sum(array)
y = [np.array((1, 2, 3))] >> my_sum
func_pipe
object
Partially apply a You can partially apply a func_pipe
object as follows:
from dpl.pipe import func_pipe
@func_pipe
def my_sum(x, y, z):
return x + y + z
y = (1, 2) >> my_sum.partial(z=3)
and that is equivalent to:
def my_sum(x, y, z):
return x + y + z
y = my_sum(x=1, y=2, z=3)
The same way is applied for keyword parameters.
from dpl.pipe import func_pipe
@func_pipe
def my_sum(x, y, z):
return x + y + z
y = {'x': 1, 'y': 2} >> my_sum.partial(z=3)
Function composition as a function chain
Assume that you have the following functions:
def add_1(x):
return x + 1
def multiply_2(x):
return x * 2
def subtract_3(x):
return x - 3
and you want to compose them to calculate a value:
y = subtract_3(
multiply_2(
add_1(1)))
You compute y
by adding 1 to x=1
, then multiplying the result by 2, and finally subtracting the previous result by 3 but you write the code in the following order: subtract_3
, multiply_2
, and add_1
. With the helper dpl.pipe.chain_pipe
, we can rewrite it a more natural order:
from dpl.pipe import chain_pipe
y = 1 | chain_pipe(add_1, multiply_2, subtract_3)
The third last in the above code will do exactly the same thing as the way we compose three functions but it is much easier to read.
Another advantage of chain_pipe
is that you can chain the lambda
functions:
from dpl.pipe import chain_pipe
y = 1 | chain_pipe(
lambda x: x + 1,
lambda x: x * 2,
lambda x: x - 3
)
You can also control how to pass the arguments between the functions in the chain pipe by setting the keyword parameter unpacked
(default False
). The meaning of unpacking arguments while passing is described in the section of func_pipe
.
For example, the following code:
from dpl.pipe import chain_pipe
y = (1, 2) >> chain_pipe(
lambda x: x + 1, x + 2,
lambda x, y: x * 2, y * 2,
lambda x, y: x - 3, y - 3,
unpacked=True
)
is equivalent to:
def add_1_2(x, y):
return x + 1, y + 2
def multiply_2(x, y):
return x * 2, y * 2
def subtract_3(x, y):
return x - 3, y - 3
y = subtract_3(
*multiply_2(
*add_1_2(1, 2)))
pipe_class
decorator
This decorator will turns all instance methods into func_pipe
objects that you can use to build pipelines later.
For example:
from dpl.pipe import pipe_class
@pipe_class
class MyClass(object):
def add_1(self, x):
return x + 1
def multiply_2(self, x):
return x * 2
def subtract_3(self, x):
return x - 3
def main(self):
y = 1 | self.add_1 | self.multiply_2 | self.subtract_3
is equivalent to:
class MyClass(object):
def add_1(self, x):
return x + 1
def multiply_2(self, x):
return x * 2
def subtract_3(self, x):
return x - 3
def main(self):
y = self.subtract_3(
self.multiply_2(
self.add_1(1)))
map_pipe
, reduce_pipe
, filter_pipe
function
Those are helpers for writing codes with traditional map
, reduce
, filter
functions in a more readable way. The map_pipe
function accepts a function which is the function will be applied on all elements of an array and return a func_pipe
object that you can use to build pipelines later.
For example:
The following code
from dpl.pipe import map_pipe
y = (1, 2, 3) | map_pipe(lambda x: x + 1)
is equivalent to:
y = map(lambda x: x + 1, (1, 2, 3))
The same principle is applied for reduce_pipe
and filter_pipe
.
ConstantInstanceVariables
class
Sometimes, you want to force all instance variables to be constants or cannot be reassigned so that you guarantee that there is no place in you code changing those variables to avoid some bugs. This can be done by subclassing the class ConstantInstanceVariables
.
For example, the following code:
from dpl.constant import ConstantInstanceVariables
class MyClass(ConstantInstanceVariables):
def __init__(self, value):
self._value = value
def main(self):
self._value = 1
MyClass(10).main()
will raise the error
ConstantError: Cannot rebind constant "_value"
as I am trying to rebind the instance variable self._value
in the main method.
Converting return results into constant objects
When a function returns a tuple of results, the code using this result will need to know the position of each arguments in the tuple result. For example:
def add_1(x, y, x):
x_prime = x + 1
y_prime = y + 1
z_prime = z + 1
return x_prime, y_prime, z_prime
y_prime = add_1(1, 2, 3)[1]
It is much easier to use the result if we combine the result variables into an immutable object. For example:
from dpl.converter import vars_to_object
def add_1(x, y, z):
x_prime = x + 1
y_prime = y + 1
z_prime = z + 1
return vars_to_object(x_prime, y_prime, z_prime, local_dict=locals())
y_prime = add_1(1, 2, 3).y_prime
Remember to add the part local_dict=locals()
when calling the function vars_to_object
.
You can also convert a dictionary to an object or a zip
pair. For example:
from dpl.converter import dict_to_object
def add_1(x, y, z):
x_prime = x + 1
y_prime = y + 1
z_prime = z + 1
return dict_to_object({
'x_prime': x_prime,
'y_prime': y_prime,
'z_prime': z_prime
})
y_prime = add_1(1, 2, 3).y_prime
or
from dpl.converter import zip_to_object
def add_1(x, y, z):
x_prime = x + 1
y_prime = y + 1
z_prime = z + 1
return zip_to_object(
('x_prime', 'y_prime', 'z_prime'),
(x_prime, y_prime, z_prime))
y_prime = add_1(1, 2, 3).y_prime
Caching a function result
Sometimes, it is useful to cache the result of a heavily computing function (or instance method). You can cache a function result by using the decorator dpl.cache.func_cache
as follows:
from dpl.cache import func_cache
@func_cache
def add_1(x):
print(f'calling add_1 on x={x}')
return x + 1
for i in range(3):
y = add_1(1)
print(f'i = {i}, y = {y}')
You will see the following result:
calling add_1 on x=1
i = 0, y = 2
i = 1, y = 2
i = 2, y = 2
which means the function add_1
is called to compute the result for x=1
only once.