In newer versions of Python we have two not much used features: ellipsis:
>>>print(...)Ellipsis
And matrix multiplication operator:
classDummy(str):def__matmul__(self,other):print('{}@{}'.format(self,other))>>>Dummy('ok')@'there'ok@there
So let’s start with ...
, in Scala we can partially apply (or curry) function with _
:
defadd(x:Int,y:Int)=x+yvaladdOne=add(1,_:Int)addOne(5)6:Int
Wouldn’t it be nice to have similar option in Python, like:
defadd(x,y):returnx+yaddFive=add(...,5)
And we can easily implement it with some decorator:
fromfunctoolimportwrapsclassPartial:def__init__(self,fn,args,kwargs):self._fn=fnself._args=argsself._kwargs=kwargsdef__call__(self,replacement):args=[replacementifargis...elseargforarginself._args]kwargs={key:replacementifvalis...elsevalforkey,valinself._kwargs.items()}returnself._fn(*args,**kwargs)def__repr__(self):return'<Partial: {}(*{}, **{})>'.format(self._fn.__name__,repr(self._args),repr(self._kwargs))defellipsis_partial(fn):@wraps(fn)defwrapper(*args,**kwargs):ellipsises=(list(args)+list(kwargs.values())).count(...)ifellipsises>1:raiseTypeError('Only one ... allowed as an argument.')elifellipsises:returnPartial(fn,args,kwargs)else:returnfn(*args,**kwargs)returnwrapper
So here if we find ...
in arguments, we return Partial
object. And
when the object called – we replace ...
with passed value. In action:
@ellipsis_partialdefadd(x,y):returnx+yaddFive=add(5,...)>>>addFive(10)15
And it works! So back to matrix multiplication operator. In F# there’s nice piping operators:
>[1..10]|>List.filter(funx->x%3=0)valit:intlist=[3;6;9]
So with our operator in Python it should look like:
range(1,10)@filter(lambdax:x%3==0,...)
And we can easily implement it just by adding __rmatmul__
to Partial
:
classPartial:def__init__(self,fn,args,kwargs):self._fn=fnself._args=argsself._kwargs=kwargsdef__call__(self,replacement):args=[replacementifargis...elseargforarginself._args]kwargs={key:replacementifvalis...elsevalforkey,valinself._kwargs.items()}returnself._fn(*args,**kwargs)def__rmatmul__(self,replacement):returnself(replacement)def__repr__(self):return'<Partial: {}(*{}, **{})>'.format(self._fn.__name__,repr(self._args),repr(self._kwargs))
And in action:
filter=ellipsis_partial(filter)to_list=ellipsis_partial(list)>>>range(1,10)@filter(lambdax:x%3==0,...)@to_list(...)[3,6,9]
And we can use it in even more complex cases:
map=ellipsis_partial(map)join=ellipsis_partial(str.join)>>>range(1,10)@map(lambdax:x+4,...) \
@filter(lambdax:x%3==0,...) \
@map(str,...) \
@join(', ',...)6,9,12
But it’s a bit not nice to wrap all callables in ellipsis_partial
,
we can use some hacks with inspect
or module loaders to doing it automatically, but it’s too magic
for me. So we can add little function that wrap and call:
def_(fn,*args,**kwargs):returnellipsis_partial(fn)(*args,**kwargs)
Usage:
fromfunctoolsimportreduce>>>range(1,10)@map(lambdax:x+4,...) \
@filter(lambdax:x%3==0,...) \
@_(reduce,lambdax,y:x*y,...) \
@_('value: {}'.format,...)value:648
However it may look strange and unpythonic, but I guess it would be nice to see something like this in future Python releases.