Allow natural function notations like (1,2) *dot* (3,4) for dot((1,2), (3,4)) or 1 /frac/ 3 for Fraction(1,3), pipes and other useful operators to functions.
Project description
Always wanted to add custom operators to your functions ?
a = 2 + (1,2,3) /dot/ (4,5,6) # a = 2 + dot((1,2,3), (4,5,6))
Y = [1,2,7,0,2,0] |no_zero |plus(1) |to(set) # Y == {2,3,8}
square = elipartial(pow, ..., 2) # square = lambda x: pow(x, 2)
display = hex *compose* ord # display = lambda x: hex(ord(x))
This example shows how infix operators can be created, the library also introduces bash like pipes and shortcuts to create partial functions or function composition inspired by functional languages.
Using infix
Infix operators can be created using the infix
class.
It works for existing functions, like numpy.dot
:
import numpy
dot = infix(numpy.dot)
a = (1,2,3) /dot/ (4,5,6) # use as an infix
If you already have dot
in your namespace, don't worry, it still works as a function:
a = dot((1,2,3), (4,5,6)) # still works as a function
Or for custom functions as a decorator:
@infix
def crunch(x,y):
"""
Do a super crunchy operation between two numbers.
"""
return x + 2 * y
a = 1 |crunch| 2 # a = crunch(1, 2)
a = crunch(1, 2) # still works
help(crunch.function) # to get help about the initial function
Any binary operator can be used, 1 |f| 2
can be written 1 *f* 2
, 1 %f% 2
or 1 << f << 2
but /
or |
should be clean for all use cases.
Beware if you use **
, the operator is right to left:
b = 1 **f** 2 # a = f(2, 1)
Useful for dot and cross product
Dot and cross products are used heavily in mathematics and physics as an infix operator ·
or ×
.
import numpy
dot = infix(numpy.dot)
a = (1,2,3) /dot/ (4,5,6)
a = (1,2,3) |dot| (4,5,6) # same
r = 2 + (1,2,3) /dot/ (4,5,6) # here "/" has priority over "+" like in normal python
r = 2 + (1,2,3) *dot* (4,5,6) # for a dot PRODUCT, "*" seems logical
r = 2 + dot((1,2,3), (4,5,6)) # still works as a function
cross = infix(numpy.cross)
tau = (1,2) /cross/ (3,4)
Z = (1,2,3) /cross/ (4,5,6)
Using |
for low priority
In some use cases, one want to mix classic operators with function operators,
the |
operator may be used as a low priority operator.
Y = A + B |dot| C # is parsed as Y = (A + B) |dot| C
Y = A + B /dot/ C # is parsed as Y = A + (B /dot/ C)
Useful for fractions
When using the fractions
module, often you want to transition from float
to Fraction
.
Your current code uses /
for division and you can just replace the slashes with /frac/
, the expression stays natural to read.
from fractions import Fraction
frac = infix(Fraction)
a = 1 + 1 / 3 # 1.3333...
a = 1 + 1 /frac/ 3 # Fraction(4, 3)
b = 2 * Fraction(a + 3, a + 1) # very different from '(a + 3) / (a + 1)'
b = 2 * (a + 3) /frac/ (a + 1) # almost identical to '(a + 3) / (a + 1)'
Useful for ranges, do you like 2..5
in ruby?
In many languages, iterating over a range has a notational shortcut, like 2..5
in ruby.
Now you can even write for i in 1 /inclusive/ 5
in python.
@infix
def inclusive(a,b):
return range(a, b+1)
for i in 2 /inclusive/ 5:
print(i) # 2 3 4 5
for i in inclusive(2, 5):
print(i) # 2 3 4 5
However, redefining range = infix(range)
is a bad idea because it would break code like isinstance(x, range)
.
In that particuliar example, I would choose exclusive = infix(range)
.
Useful for isinstance, do you like instanceof
in Java and Js?
In Java and Javascript, testing the class of an object is done via x instanceof Class
,
the python builtin isinstance
could be enhanced with infix notation or be renamed to instanceof
.
isinstance = infix(isinstance)
assert 1 /isinstance/ int
assert [] /isinstance/ (list, tuple)
assert 1 / 2 |isinstance| float
Useful for pipes: postfix (alias to)
In bash, a functionality called pipes is useful to reuse an expression and change the behavior by just adding code at the end. The library can be used for that.
@postfix
def no_zero(L):
return [x for x in L if x != 0]
@postfix
def plus_one(L):
return [x+1 for x in L]
Y = [1,2,7,0,2,0] |no_zero |plus_one # Y == [2,3,8,3]
Y = plus_one(no_zero([1,2,7,0,2,0])) # Y == [2,3,8,3]
Using to = postfix
makes it quite readable in a pipe.
>>> from funcoperators import postfix as to # funcoperators version 0.x
>>> from funcoperators import to # funcoperators version 1.x
>>> 'hello' |to(str.upper) |to(lambda x:x + '!') |to('I said "{}"'.format) |to(print)
I said "HELLO!"
>>> print('I said "{}"'.format('hello'.upper() + '!'))
I said "HELLO!"
Pipes with arguments: pipe factory
Sometimes, pipes want extra information, for example in our last example, no_zero
is a special case of a pipe that filters out a value,
use the pipe factory recipe like so:
def filter_out(x):
@postfix
def f(L):
return [y for y in L if y != x]
return f
# shorter with a lambda
def filter_out(x):
return postfix(lambda L:[y for y in L if y != x])
L = [1,2,7,0,2,0] | filter_out(0) # L == [2,3,8,3]
from funcoperators import mapwith
s = '1 2 7 2'.split() | mapwith(int) | to(sum) # s = 12 = sum(map(int, '1 2 7 2'.split()))
from funcoperators import mapwith as Map
from funcoperators import filterwith as Filter
S = '1 2 7 2'.split() | Map(int) | Filter(lambda x:x < 5) | to(set)
Useful for format and join
>>> 42 | format /curryright/ 'x'
'2a'
>>> formatwith = lambda fmt: postfix(lambda value: format(value, fmt))
>>> 52 |formatwith('x')
'2a'
>>> from funcoperators import to
>>> 3.1415 |to('{:.02}'.format)
'3.1'
>>> [1, 2, 7, 2] |mapwith(str) |to('/'.join) |to(print)
1/2/7/2
Function composition (compose, alias circle)
In mathematics and functional programming languages, function composition is naturally used using a circle
operator to write things like h = f ∘ g
.
s = hex(ord('A')) # s = '0x41'
from funcoperators import compose
display = hex /compose/ ord
s = display('A') # s = '0x41'
display = hex *circle* ord
from funcoperators import compose as o
display = hex -o- ord # looks like a dot
compose
can have more than two functions.
f = compose(str.upper, hex, lambda x:x+1, ord) # simple function
f = str.upper /compose/ hex /compose/ (lambda x:x+1) /compose/ ord # operator
Using call for inline compose
>>> print(5 + 2 * 10, 'B', sep='/')
25
>>> print |call(5 + 2 * 10, 'B', sep='/')
25/B
>>> print(','.join('abcdef' + 3 * 'x'))
a,b,c,d,e,f,x,x,x
>>> print *compose* ' '.join |call('abcdef' + 3 * 'x')
a,b,c,d,e,f,x,x,x
>>> compose(print, ','.join) |call('abcdef' + 3 * 'x')
a,b,c,d,e,f,x,x,x
>>> len |infixcall| 'hallo' * 3
15
>>> print |callargs| ('a', 5)
a 5
More partial syntax
The library adds sugar to functools.partial, using functions called curry
(and variants like curryright
, simplecurry
) and partially
. The name curry
comes from other languages.
def f(x,y,z):
return x + y + z
from funcoperators import curry
g = f /curry/ 5
y = f(2,1) # y = 8
from funcoperators import curryright
square = pow /curryright/ 2 # square(x) = x ** 2
square = curryright(pow, 2) # square(x) = x ** 2
from funcoperators import provide_right # alias provide_right = curryright
square = provide_right(pow, 2) # square(x) = x ** 2
square = pow /provide_right/ 2 # square(x) = x ** 2
from funcoperators import simplecurry
g = f |simplecurry(1, z=3)
y = g(2)
partially
allows to upgrade a function to provide methods like f.partial
and provides f[arg]
to curry.
from funcoperators import partially
@partially
def f(x,y,z):
return x - y + 2 * z
r = f(1,2,3)
g = f[1] # g = a function with two arguments: y,z
r = g(2,3)
r = f[1](2,3)
r = f[1][2][3]()
# Notice that "f[1,2]" doesn't work because it gives only one argument: a tuple (@see partiallymulti)
g = f[1] # gives positional arguments
g = f.val(1) # gives positional arguments
g = f.key(z=3) # gives keyword arguments
g = f.partial(1, z=3) # gives positional and keyword arguments
# alias part = assuming = given = where = partial
g = f.part(1, z=3)
g = f.where(1, z=3)
g = f.given(1, z=3)
partiallymulti
allows f[arg1, arg2]
.
from funcoperators import partiallymulti
@partiallymulti
def f(x,y,z):
return x - y + 2 * z
r = f(1,2,3)
g = f[1,2] # g = a function with one argument: z
r = g(3)
Using partiallyauto
In functional languages, function composition is sometimes not dissociable from function call,
partiallyauto
only works for methods with N fixed positional arguments.
@partiallyauto
def f(x,y,z):
return x - y + 2 * z
r = f(1,2,3) # r = 6
r = f(1)(2)(3) # r = 6
r = f(1)(2,3) # r = 6
g = f(1) # g = a function with two arguments
r = g(2,3) # r = 6
k = g(2) # k = a function with one argument
Using Ellipsis
Python's functools.partial
only works for arguments that will be provided later, one must use keywords arguments.
However, not all functions accept keywords arguments, like the builtin pow
, one can use curryright
because pow only has two arguments.
square = curryright(pow, 2) # square(x) = x ** 2
The library also proposes to use Python's ...
(Ellipsis
) as a natural placeholder for arguments.
The functions using this convention have a name beginning with eli
.
tenexp = elipartial(pow, 10) # = pow(10, something)
y = tenexp(2) # 10 ** 2
square = elipartial(pow, ..., 2) # = pow(something, 2)
y = square(5) # 5 ** 2
square = pow |elicurry(..., 2) # = pow(something, 2)
y = square(5) # 5 ** 2
If you like the partially
and partiallymulti
syntax, there is bracket
that has all the concepts in one class.
@bracket
def f(x,y,z):
return x - y + 2 * z
r = f(1,2,3)
g = f[1, ..., 3] # g = a function with one argument: y
r = g(2)
g = f.partial(1, ..., 3) # as a method
g = f.partial(1, z=3) # allowing keyword arguments
Here is a more complex example using elicurry
, we define show
to be the print
function with arguments 1
, something, 3
and keyword argument sep='/'
.
show = print |elicurry(1, ..., 3, sep='/')
show(2) # prints 1/2/3
Let's note that elicurry
has many aliases:
show = print |elicurry(1, ..., 3, sep='/')
show = print |with_arguments(1, ..., 3, sep='/')
show = print |deferredcall(1, ..., 3, sep='/')
show = print |latercall(1, ..., 3, sep='/')
More examples
See more examples in the test cases in source code.
Release Notes
Version 1.0 created some non backward-compatible change (call
) and included useful use cases (to
, mapwith
)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file funcoperators-1.1.3.tar.gz
.
File metadata
- Download URL: funcoperators-1.1.3.tar.gz
- Upload date:
- Size: 15.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.19.1 setuptools/39.2.0 requests-toolbelt/0.8.0 tqdm/4.23.4 CPython/3.6.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 49a2c48bfa99048863668130df1e2bc4741c57c69052e4f3983e949e870b0c0a |
|
MD5 | b5e1d36c37d9ca56fd17e260466a5897 |
|
BLAKE2b-256 | 757931866b4aee1024a1ea75dfab431e555cf7ae15f7c349877526cc0ffae5f2 |