Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Vladimir Iakolev: Partial application and piping with ... and @

$
0
0

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.

Gist with sources.


Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>