python functools partial confusion

Viewed 344

consider the following:

from functools import partial
def add(a, b, c):
   return 100 * a + 10 * b + c

add_part = partial(add, c = 2, b = 1)
add_part(3)
312

Works fine. However:

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

bar = partial(foo, y=3)

bar(1, 2)

barfs:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'y'

Obviously I am missing something obvious, but what?

2 Answers

The definition of partial() from the official functools documentation is:

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

That means that in your case partial() returns the foo() function with the signature modified as follow:

>>> from inspect import signature
>>> signature(bar)
<Signature (x, *, y=3, z)>

To solve your error, you could provide keyword arguments to the bar() function:

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

bar = partial(foo, y=3)

bar(x=1, z=2)

From the python 3.9 documentation:

Return a new partial object which when called will behave like func called with the positional arguments args and keyword arguments keywords. If more arguments are supplied to the call, they are appended to args. If additional keyword arguments are supplied, they extend and override keywords.

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

bar = partial(foo, y=3)

print(bar.args) # ()
print(bar.keywords) # {'y': 3}

When bar(1, 2) is called, bar.args becomes (1, 2) and bar.keywords is still {'y': 3}

Taking note of this, the next thing is to refer to "Function Calling Behavior" as specified in PEP 3102

When a function is called, the input arguments are assigned to formal parameters as follows:

  • For each formal parameter, there is a slot which will be used to contain the value of the argument assigned to that parameter.
  • Slots which have had values assigned to them are marked as 'filled'. Slots which have no value assigned to them yet are considered 'empty'.
  • Initially, all slots are marked as empty.
  • Positional arguments are assigned first, followed by keyword arguments.
  • For each positional argument:
    • Attempt to bind the argument to the first unfilled parameter slot. If the slot is not a vararg slot, then mark the slot as 'filled'.
    • If the next unfilled slot is a vararg slot, and it does not have a name, then it is an error.
    • Otherwise, if the next unfilled slot is a vararg slot then all remaining non-keyword arguments are placed into the vararg slot.
  • For each keyword argument:
    • If there is a parameter with the same name as the keyword, then the argument value is assigned to that parameter slot. However, if the parameter slot is already filled, then that is an error.
    • Otherwise, if there is a 'keyword dictionary' argument, the argument is added to the dictionary using the keyword name as the dictionary key, unless there is already an entry with that key, in which case it is an error.
    • Otherwise, if there is no keyword dictionary, and no matching named parameter, then it is an error.
  • Finally:
    • If the vararg slot is not yet filled, assign an empty tuple as its value.
    • For each remaining empty slot: if there is a default value for that slot, then fill the slot with the default value. If there is no default value, then it is an error.

I'm not quite sure though if the above is up to date or how to apply it in this situation because I'm not sure if bar has positional parameters or varargs.

Related