Using partial
with a Positional Argument
f = partial(bar, 3)
By design, upon calling a function, positional arguments are assigned first. Then logically, 3
should be assigned to a
with partial
. It makes sense to remove it from the signature as there is no way to assign anything to it again!
when you have f(a=2, b=6)
, you are actually doing
bar(3, a=2, b=6)
when you have f(2, 2)
, you are actually doing
bar (3, 2, 2)
We never get rid of 3
For the new partial function:
- We can't give
a
a different value with another positional argument
- We can't use the keyword
a
to assign a different value to it as it is already "filled"
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.
I recommend reading the function calling behavior section of pep-3102 to get a better grasp of this matter.
Using partial
with a Keyword Argument
f = partial(bar, b=3)
This is a different use case. We are applying a keyword argument to bar
.
You are functionally turning
def bar(a, b):
...
into
def f(a, *, b=3):
...
where b
becomes a keyword-only argument
instead of
def f(a, b=3):
...
inspect.signature
correctly reflects a design decision of partial
. The keyword arguments passed to partial
are designed to append additional positional arguments (source).
Note that this behavior does not necessarily override the keyword arguments supplied with f = partial(bar, b=3)
, i.e., b=3
will be applied regardless of whether you supply the second positional argument or not (and there will be a TypeError
if you do so). This is different from a positional argument with a default value.
>>> f(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() takes 1 positional argument but 2 were given
where f(1, 2)
is equivalent to bar(1, 2, b=3)
The only way to override it is with a keyword argument
>>> f(2, b=2)
An argument that can only be assigned with a keyword but positionally? This is a keyword-only argument. Thus (a, *, b=3)
instead of (a, b=3)
.
The Rationale of Non-default Argument follows Default Argument
f = partial(bar, a=3)
assert str(signature(f)) == '(*, a=3, b)' # whaaa?! non-default argument follows default argument?
- You can't do
def bar(a=3, b)
. a
and b
are so called positional-or-keyword arguments
.
- You can do
def bar(*, a=3, b)
. a
and b
are keyword-only arguments
.
Even though semantically, a
has a default value and thus it is optional, we can't leave it unassigned because b
, which is a positional-or-keyword argument
needs to be assigned a value if we want to use b
positionally. If we do not supply a value for a
, we have to use b
as a keyword argument
.
Checkmate! There is no way for b
to be a positional-or-keyword argument
as we intended.
The PEP for positonal-only arguments also kind of shows the rationale behind it.
This also has something to do with the aforementioned "function calling behavior".
partial
!= Currying & Implementation Details
partial
by its implementation wraps the original function while storing the fixed arguments you passed to it.
IT IS NOT IMPLEMENTED WITH CURRYING. It is rather partial application instead of currying in the sense of functional programming. partial
is essentially applying the fixed arguments first, then the arguments you called with the wrapper:
def __call__(self, /, *args, **keywords):
keywords = {**self.keywords, **keywords}
return self.func(*self.args, *args, **keywords)
This explains f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'
.
See also: Why is partial
called partial
instead of curry
Under the Hood of inspect
The outputs of inspect is another story.
inspect
itself is a tool that produces user-friendly outputs. For partial()
in particular (and partialmethod()
, similarly), it follows the wrapped function while taking the fixed parameters into account:
if isinstance(obj, functools.partial):
wrapped_sig = _get_signature_of(obj.func)
return _signature_get_partial(wrapped_sig, obj)
Do note that it is not inspect.signature
's goal to show you the actual signature of the wrapped function in the AST.
def _signature_get_partial(wrapped_sig, partial, extra_args=()):
"""Private helper to calculate how 'wrapped_sig' signature will
look like after applying a 'functools.partial' object (or alike)
on it.
"""
...
So we have a nice and ideal signature for f = partial(bar, 3)
but get f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'
in reality.
Follow-up
If you want currying so badly, how do you implement it in Python, in the way which gives you the expected TypeError
?