328

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?

Innat
  • 16,113
  • 6
  • 53
  • 101
user1865341
  • 4,099
  • 5
  • 18
  • 15
  • 1
    Simple answer: it doesn't! It's actually passed as `y`. Try adding the line `print(f'x: {x}, y: {y}')` to the top of `sum2` and you'll see, Check @MSK's answer below. – Ricardo Oct 11 '21 at 09:04

8 Answers8

382

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

def partial(func, *part_args):
    def wrapper(*extra_args):
        args = list(part_args)
        args.extend(extra_args)
        return func(*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).

bereal
  • 32,519
  • 6
  • 58
  • 104
  • 2
    from where did u get the `extra_args` variable – user1865341 Mar 11 '13 at 05:40
  • 3
    `extra_args` is something that passed by the partial caller, in the example with `p = partial(func, 1); f(2, 3, 4)` it is `(2, 3, 4)`. – bereal Mar 11 '13 at 05:42
  • 1
    but why we would do that , any special use case where something has to be done by partial only and can't be done with other thing – user1865341 Mar 11 '13 at 05:43
  • @user1865341 I added an example to the answer. – bereal Mar 11 '13 at 05:52
  • with your example , what is the relation between `callback` and `my_callback` – user1865341 Mar 11 '13 at 06:06
  • After `my_callback` is wrapped with partial it will mimic `callback`'s API and you may call `my_callback` wherever could call `callback`. – bereal Mar 11 '13 at 06:11
  • Sorry i am getting little bit . iam 70% clear but still not 100% clear. can you give some more code example for current scenario like u did before. . i get to the point that `partial(my_callback, context)(event, params) == my_callback(context, event, params)` but i am not able to get , how can we use that where `callback is reuired` – user1865341 Mar 11 '13 at 06:16
  • Well callback is not a function to be replaced, it's more of an API description. I've updated the example code to reflect some semi-real-life situation. – bereal Mar 11 '13 at 07:24
  • sorry for the trouble . can you tell me how the solution would look like for the other two options like `object creating` and lambda. i am not able to comprehend yet. i am new to python – user1865341 Mar 11 '13 at 09:20
  • @user1865341 I updated the answer with object and lambda solutions. – bereal Mar 11 '13 at 10:14
  • In your example: >>> incr2 = functools.partial(sum2, 1) >>> incr2(4) Am i right in thinking, the 4 is extra_args, and 1 is part_args, so effectively the incr2(4) == sum2(1, 4) – Can Lu Jan 08 '14 at 06:42
  • @CanLu yes, or, more general, `partial(f, x)(y) == f(x, y)` for all `f`,`x`,`y`. – bereal Jan 08 '14 at 07:08
  • Does this perform any constant folding to achieve performance gains, like currying in functional languages? – Brent Aug 28 '18 at 20:14
  • @Brent I don't think so, that would require static analysis, while in Python types are unknows until the execution. – bereal Aug 29 '18 at 03:33
  • Good answer, though IMHO it's important to note the role of closures, as in ["Closures, Partials and Decorators"](https://stackoverflow.com/questions/37055508/closures-partials-and-decorators). – user1071847 May 17 '19 at 15:30
  • This line seems like it should be deleted: `context = get_context() # whatever`. It's not part of any of the other examples, and it adds an extra line to just the `partial` example which is omitted from all the other examples for some reason, kinda obscuring the fact that the `partial` example is actually just one line versus the two or more of the others. – mtraceur Sep 05 '22 at 06:23
168

partials are incredibly useful.

For instance, in a 'pipe-lined' sequence of function calls (in which the returned value from one function is the argument passed to the next).

Sometimes a function in such a pipeline requires a single argument, but the function immediately upstream from it returns two values.

In this scenario, functools.partial might allow you to keep this function pipeline intact.

Here's a specific, isolated example: suppose you want to sort some data by each data point's distance from some target:

# create some data
import random as RND
fnx = lambda: RND.randint(0, 10)
data = [ (fnx(), fnx()) for c in range(10) ]
target = (2, 4)

import math
def euclid_dist(v1, v2):
    x1, y1 = v1
    x2, y2 = v2
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

To sort this data by distance from the target, what you would like to do of course is this:

data.sort(key=euclid_dist)

but you can't--the sort method's key parameter only accepts functions that take a single argument.

so re-write euclid_dist as a function taking a single parameter:

from functools import partial

p_euclid_dist = partial(euclid_dist, target)

p_euclid_dist now accepts a single argument,

>>> p_euclid_dist((3, 3))
  1.4142135623730951

so now you can sort your data by passing in the partial function for the sort method's key argument:

data.sort(key=p_euclid_dist)

# verify that it works:
for p in data:
    print(round(p_euclid_dist(p), 3))

    1.0
    2.236
    2.236
    3.606
    4.243
    5.0
    5.831
    6.325
    7.071
    8.602

Or for instance, one of the function's arguments changes in an outer loop but is fixed during iteration in the inner loop. By using a partial, you don't have to pass in the additional parameter during iteration of the inner loop, because the modified (partial) function doesn't require it.

>>> from functools import partial

>>> def fnx(a, b, c):
      return a + b + c

>>> fnx(3, 4, 5)
      12

create a partial function (using keyword arg)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(b=4, c=5)
     21

you can also create a partial function with a positional argument

>>> pfnx = partial(fnx, 12)

>>> pfnx(4, 5)
      21

but this will throw (e.g., creating partial with keyword argument then calling using positional arguments)

>>> pfnx = partial(fnx, a=12)

>>> pfnx(4, 5)
      Traceback (most recent call last):
      File "<pyshell#80>", line 1, in <module>
      pfnx(4, 5)
      TypeError: fnx() got multiple values for keyword argument 'a'

another use case: writing distributed code using python's multiprocessing library. A pool of processes is created using the Pool method:

>>> import multiprocessing as MP

>>> # create a process pool:
>>> ppool = MP.Pool()

Pool has a map method, but it only takes a single iterable, so if you need to pass in a function with a longer parameter list, re-define the function as a partial, to fix all but one:

>>> ppool.map(pfnx, [4, 6, 7, 8])
doug
  • 69,080
  • 24
  • 165
  • 199
80

short answer, partial gives default values to the parameters of a function that would otherwise not have default values.

from functools import partial

def foo(a,b):
    return a+b

bar = partial(foo, a=1) # equivalent to: foo(a=1, b)
bar(b=10)
#11 = 1+10
bar(a=101, b=10)
#111=101+10
Alex Fortin
  • 2,105
  • 1
  • 18
  • 27
55

Partials can be used to make new derived functions that have some input parameters pre-assigned

To see some real world usage of partials, refer to this really good blog post here

A simple but neat beginner's example from the blog, covers how one might use partial on re.search to make code more readable. re.search method's signature is:

search(pattern, string, flags=0) 

By applying partial we can create multiple versions of the regular expression search to suit our requirements, so for example:

is_spaced_apart = partial(re.search, '[a-zA-Z]\s\=')
is_grouped_together = partial(re.search, '[a-zA-Z]\=')

Now is_spaced_apart and is_grouped_together are two new functions derived from re.search that have the pattern argument applied(since pattern is the first argument in the re.search method's signature).

The signature of these two new functions(callable) is:

is_spaced_apart(string, flags=0)     # pattern '[a-zA-Z]\s\=' applied
is_grouped_together(string, flags=0) # pattern '[a-zA-Z]\=' applied

This is how you could then use these partial functions on some text:

for text in lines:
    if is_grouped_together(text):
        some_action(text)
    elif is_spaced_apart(text):
        some_other_action(text)
    else:
        some_default_action()

You can refer the link above to get a more in depth understanding of the subject, as it covers this specific example and much more..

sisanared
  • 4,175
  • 2
  • 27
  • 42
  • 1
    Isn't this equivalent to `is_spaced_apart = re.compile('[a-zA-Z]\s\=').search`? If so, is there a guarantee that the `partial` idiom compiles the regular expression for faster reuse? – Aristide May 07 '20 at 08:33
  • 2
    Commenting because edit queue is full. The link is dead. It can be replaced with https://web.archive.org/web/20160920124126/https://chriskiehl.com/article/Cleaner-coding-through-partially-applied-functions/ – user47 Jul 28 '22 at 14:39
18

In my opinion, it's a way to implement currying in python.

from functools import partial
def add(a,b):
    return a + b

def add2number(x,y,z):
    return x + y + z

if __name__ == "__main__":
    add2 = partial(add,2)
    print("result of add2 ",add2(1))
    add3 = partial(partial(add2number,1),2)
    print("result of add3",add3(1))

The result is 3 and 4.

Hanzhou Tang
  • 351
  • 2
  • 6
  • 3
    Hmm, not exactly. Currying is dividing a function with n parameters into n successive functions with one parameter. Partial application is 'pre-filling' a function with some parameters, then returning a function with a smaller number of parameters – Andrew Magerman Oct 13 '21 at 11:38
3

This answer is more of an example code. All the above answers give good explanations regarding why one should use partial. I will give my observations and use cases about partial.

from functools import partial
 def adder(a,b,c):
    print('a:{},b:{},c:{}'.format(a,b,c))
    ans = a+b+c
    print(ans)
partial_adder = partial(adder,1,2)
partial_adder(3)  ## now partial_adder is a callable that can take only one argument

Output of the above code should be:

a:1,b:2,c:3
6

Notice that in the above example a new callable was returned that will take parameter (c) as it's argument. Note that it is also the last argument to the function.

args = [1,2]
partial_adder = partial(adder,*args)
partial_adder(3)

Output of the above code is also:

a:1,b:2,c:3
6

Notice that * was used to unpack the non-keyword arguments and the callable returned in terms of which argument it can take is same as above.

Another observation is: Below example demonstrates that partial returns a callable which will take the undeclared parameter (a) as an argument.

def adder(a,b=1,c=2,d=3,e=4):
    print('a:{},b:{},c:{},d:{},e:{}'.format(a,b,c,d,e))
    ans = a+b+c+d+e
    print(ans)
partial_adder = partial(adder,b=10,c=2)
partial_adder(20)

Output of the above code should be:

a:20,b:10,c:2,d:3,e:4
39

Similarly,

kwargs = {'b':10,'c':2}
partial_adder = partial(adder,**kwargs)
partial_adder(20)

Above code prints

a:20,b:10,c:2,d:3,e:4
39

I had to use it when I was using Pool.map_async method from multiprocessing module. You can pass only one argument to the worker function so I had to use partial to make my worker function look like a callable with only one input argument but in reality my worker function had multiple input arguments.

Ruthvik Vaila
  • 507
  • 8
  • 17
2

Also worth to mention, that when partial function passed another function where we want to "hard code" some parameters, that should be rightmost parameter

def func(a,b):
    return a*b
prt = partial(func, b=7)
    print(prt(4))
#return 28

but if we do the same, but changing a parameter instead

def func(a,b):
    return a*b
 prt = partial(func, a=7)
    print(prt(4))

it will throw error, "TypeError: func() got multiple values for argument 'a'"

MSK
  • 41
  • 3
2

Adding couple of case from machine learning where the functional programming currying with functools.partial can be quite useful:

Build multiple models on the same dataset

the following example shows how linear regression, support vector machine and random forest regression models can be fitted on the same diabetes dataset, to predict the target and compute the score.

The (partial) function classify_diabetes() is created from the function classify_data() by currying (using functools.partial()). The later function does not require the data to be passed anymore and we can straightaway pass only the instances of the classes for the models.

from functools import partial
from sklearn.linear_model import LinearRegression
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import load_diabetes

def classify_data(data, model):
    reg = model.fit(data['data'], data['target'])
    return model.score(data['data'], data['target'])

diabetes = load_diabetes()
classify_diabetes = partial(classify_data, diabetes) # curry
for model in [LinearRegression(), SVR(), RandomForestRegressor()]:
    print(f'model {type(model).__name__}: score = {classify_diabetes(model)}')

# model LinearRegression: score = 0.5177494254132934
# model SVR: score = 0.2071794500005485
# model RandomForestRegressor: score = 0.9216794155402649

Setting up the machine learning pipeline

Here the function pipeline() is created with currying which already uses StandardScaler() to preprocess (scale / normalize) the data prior to fitting the model on it, as shown in the next example:

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

pipeline = partial(make_pipeline, StandardScaler()) # curry    
for model in [LinearRegression(), SVR(), RandomForestRegressor()]:
    print(f"model {type(model).__name__}: " \
          f"score = {pipeline(model).fit(diabetes['data'], diabetes['target'])\
                                 .score(diabetes['data'], diabetes['target'])}")

# model LinearRegression: score = 0.5177494254132934
# model SVR: score = 0.2071794500005446
# model RandomForestRegressor: score = 0.9180227193805106
Sandipan Dey
  • 21,482
  • 2
  • 51
  • 63