Search code examples
pythonfunctional-programmingcurryingfunctoolspartial-application

How does functools partial do what it does?


I am not able to get my head on how the partial works in functools. I have the following code from here:

>>> sum = lambda x, y : x + y
>>> sum(1, 2)
3
>>> incr = lambda y : sum(1, y)
>>> incr(2)
3
>>> def sum2(x, y):
    return x + y

>>> incr2 = functools.partial(sum2, 1)
>>> incr2(4)
5

Now in the line

incr = lambda y : sum(1, y)

I get that whatever argument I pass to incr it will be passed as y to lambda which will return sum(1, y) i.e 1 + y.

I understand that. But I didn't understand this incr2(4).

How does the 4 gets passed as x in partial function? To me, 4 should replace the sum2. What is the relation between x and 4?


Solution

  • Roughly, partial does something like this (apart from keyword args support etc):

    def partial(func, *part_args):
        def wrapper(*extra_args):
            return func(*args, *extra_args)            
        return wrapper
    

    So, by calling partial(sum2, 4) you create a new function (a callable, to be precise) that behaves like sum2, but has one positional argument less. That missing argument is always substituted by 4, so that partial(sum2, 4)(2) == sum2(4, 2)

    As for why it's needed, there's a variety of cases. Just for one, suppose you have to pass a function somewhere where it's expected to have 2 arguments:

    class EventNotifier(object):
        def __init__(self):
            self._listeners = []
    
        def add_listener(self, callback):
            ''' callback should accept two positional arguments, event and params '''
            self._listeners.append(callback)
            # ...
        
        def notify(self, event, *params):
            for f in self._listeners:
                f(event, params)
    

    But a function you already have needs access to some third context object to do its job:

    def log_event(context, event, params):
        context.log_event("Something happened %s, %s", event, params)
    

    So, there are several solutions:

    A custom object:

    class Listener(object):
       def __init__(self, context):
           self._context = context
    
       def __call__(self, event, params):
           self._context.log_event("Something happened %s, %s", event, params)
    
    
     notifier.add_listener(Listener(context))
    

    Lambda:

    log_listener = lambda event, params: log_event(context, event, params)
    notifier.add_listener(log_listener)
    

    With partials:

    context = get_context()  # whatever
    notifier.add_listener(partial(log_event, context))
    

    Of those three, partial is the shortest and the fastest. (For a more complex case you might want a custom object though).