passing **kwargs to a de-referencing function - python

Hope the title is conveying the correct information.
My problem is that I don't understand why call kwarg_function(some_func, a=1, b=2, c=3) fails. I would have thought that as 'c' isn't referenced with some_func() it would simply be ignored. Can anyone explain why 'c' isn't simply ignored.
def kwarg_function(function, **kwargs):
print(kwargs)
function(**kwargs)
def some_func(a, b):
print(f"type: {type(a)} values: {a}")
print(f"type: {type(b)} values: {b}")
kwarg_function(some_func, a=1, b=2) # called successfully
kwarg_function(some_func, a=1, b=2, c=3) # fails with unexpected keyword arg 'c'

Think of the ** as "unpack what's on my right side as keyword arguments" in this case.
def foo(a,b,**kwargs):
# This will take any number of arguments provided that starting from the 3rd one they are keyword args
# Those are equivalent (and working) calls
foo(1,2, x = 7)
foo(1,2, **{"x":7})
# Those will fail
foo(1,2,7)
foo(1,2, {"x":7})
The function you declared expects 2 arguments
def some_func(a, b):
And you are calling it with three under the hood, because this:
kwarg_function(some_func, a=1, b=2, c=3) # fails with unexpected keyword arg 'c'
Does this (inside kwarg_function body):
funtion(a=1,b=2,c=3)

In python, * and ** are for unpacking iterables. They don't consider what's are in them, and just unpack whatever you pass in them.
You can find more info about it in this link.
So, when you pass a=1, b=2, c=3, ... as kwargs to your kwargs_function, you will get them as kwargs param, regardless of what you have passed.
And then, when you pass **kwargs to another function, all of your data would be passed to your another function, regardless of what's in that.
If you want your some_func be more flexible with your data and accept whatever you pass to it, you can add **kwargs param to it too:
def some_func(a, b, **kwargs):
print(f"type: {type(a)} values: {a}")
print(f"type: {type(b)} values: {b}")

Related

The rationale of `functools.partial` behavior

I'm wondering what the story -- whether sound design or inherited legacy -- is behind these functools.partial and inspect.signature facts (talking python 3.8 here).
Set up:
from functools import partial
from inspect import signature
def bar(a, b):
return a / b
All starts well with the following, which seems compliant with curry-standards.
We're fixing a to 3 positionally, a disappears from the signature and it's value is indeed bound to 3:
f = partial(bar, 3)
assert str(signature(f)) == '(b)'
assert f(6) == 0.5 == f(b=6)
If we try to specify an alternate value for a, f won't tell us that we got an unexpected keyword, but rather that it got multiple values for argument a:
f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'
f(c=2, b=6) # TypeError: bar() got an unexpected keyword argument 'c'
But now if we fix b=3 through a keyword, b is not removed from the signature, it's kind changes to keyword-only, and we can still use it (overwrite the default, as a normal default, which we couldn't do with a in the previous case):
f = partial(bar, b=3)
assert str(signature(f)) == '(a, *, b=3)'
assert f(6) == 2.0 == f(6, b=3)
assert f(6, b=1) == 6.0
Why such asymmetry?
It gets even stranger, we can do this:
f = partial(bar, a=3)
assert str(signature(f)) == '(*, a=3, b)' # whaaa?! non-default argument follows default argument?
Fine: For keyword-only arguments, there can be no confusing of what parameter a default is assigned to, but I still wonder what design-thinking or constraints are behind these choices.
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?
When you provide positional or keyword arguments to partial, the new function is constructed
f = partial(bar, 3)
f(a=2, b=6) # TypeError: bar() got multiple values for argument 'a'
f(c=2, b=6) # TypeError: bar() got an unexpected keyword argument 'c'
This is actually consistent with the idea of partial, which is that arguments are passed to the wrapped function with the addition of positional and keyword arguments passed to partial
These cases behave as expected:
bar(3, a=2, b=6) # TypeError: bar() got multiple values for argument 'a'
bar(3, c=2, b=6) # TypeError: bar() got an unexpected keyword argument 'c'
But now if we fix b=3 through a keyword, b is not removed from the signature,
f = partial(bar, b=3)
assert str(signature(f)) == '(a, *, b=3)'
assert f(6) == 2.0 == f(6, b=3)
assert f(6, b=1) == 6.0
This case is different from the above because in the previous case, a positional argument was provided to partial, not a keyword argument. When positional arguments are provided to partial, then it makes sense to remove them from the signature. Arguments provided as keywords are not removed from the signature.
So far, there is no inconsistency or asymmetry.
f = partial(bar, a=3)
assert str(signature(f)) == '(*, a=3, b)' # whaaa?! non-default argument follows default argument?
The signature here makes sense and is the expectation for partial(bar, a=3) -- it works the same as def f(*, a=3, b): ... and is the correct signature in this case. Note that when you provide a=3 to partial in this case, a becomes a keyword-only argument, as does b.
This is because when a positional argument is provided as a keyword, all following arguments must be specified keyword arguments.
sig = signature(f)
sig.parameters['a'].kind # <_ParameterKind.KEYWORD_ONLY: 3>
inspect.getfullargspec(f)
# FullArgSpec(args=[], varargs=None, varkw=None, defaults=None, kwonlyargs=['a', 'b'], kwonlydefaults={'a': 3}, annotations={})

Why Does Python Allow *args After Keyword Arguments?

Example:
def foo(a, b=2, *args, **kwargs): pass
Why does this not result in a SyntaxError? *args will not catch additional non-keyword arguments because it is illegal to pass them after keyword arguments.
For python3.x the correct use of *args, **kwargs in this case looks like:
def foo(a, *args, b=2, **kwargs): pass
Thanks for any insights into this curious behavior.
Edit:
Thanks to Jab for pointing me to PEP 3102, which explains this behavior concisely. Check it out!
And also thanks to jsbueno for the additional excellent explanation, which I am updating as the best answer due to its thoroughness.
Given:
def foo(a, b=2, *args, **kwargs): pass
b is not a keyword-only parameter - it is just a parameter for which arguments can be positional or named, but have a default value. It is not possible to pass any value into args and omit passing b or passing b out of order in the signature you suggest.
This signature makes sense and is quite unambiguous - you can pass from 0 to n positional arguments, but if you pass 2 or more, the second argument is assigned to "b", and end of story.
If you pass 0 positional arguments, you can still assign values to "a" or "b" as named arguments, but trying anything like: foo(0, 1, 2, a=3, b=4) will fail as more than one value is attempted to be passed to both parameters.
Where as in:
def foo(a, *args, b=2, **kwargs): pass
it is also an unambiguous situation: the first positional argument goes to "a", the others go to "args", and you can only pass a value to "b" as a named argument.
The new / syntax in signature definition coming with Python 3.8 gives more flexibility to this, allowing one to require that "a" and "b" are passed as positional-only arguments. Again, there is no ambiguity:
def foo(a, b=2, /, *args, **kwargs): pass
A curious thing on this new syntax: one is allowed to pass named arguments to "a" and "b", but the named arguments will come up as key/value pairs inside "kwargs" - while the local variables "a" and "b" will be assigned the positional only arguments:
def foo(a, b=2, /, *args, **kwargs):
print(a, b, args, kwargs)
...
In [9]: foo(1, 2, a=3, b=4)
1 2 () {'a': 3, 'b': 4}
Whereas with the traditional syntax you ask about - def foo(a, b=2, *args, **kwargs): - one gets a TypeError if that is tried:
In [11]: foo(1,2, a=3, b=4)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-11-d002c7717dba> in <module>
----> 1 foo(1,2, a=3, b=4)
TypeError: foo() got multiple values for argument 'a'
This was implemented into 3.X for multiple reasons. Best way I can answer this is refer to
PEP 3102
Also take a look at the New Syntax section in the Python 3.0.1 docs.
TLDR:
Named parameters occurring after
*args in the parameter list must be specified using keyword syntax in the call. You can also use a bare * in the parameter list to indicate
that you don’t accept a variable-length argument list, but you do have
keyword-only arguments.

Count positional arguments in function signature

I have the following function signature:
# my signature
def myfunction(x, s, c=None, d=None):
# some irrelevant function body
I need the number of positional arguments. How can I return the number (2) of positional arguments (x & s). The number of keyword arguments is not relevant.
You can get the number of all arguments (using f.__code__.co_argcount), the number of keyword arguments (using f.__defaults__), and subtract the latter from the first:
def myfunction(x, s, c=None, d=None):
pass
all_args = myfunction.__code__.co_argcount
if myfunction.__defaults__ is not None: # in case there are no kwargs
kwargs = len(myfunction.__defaults__)
else:
kwargs = 0
print(all_args - kwargs)
Output:
2
From the Docs:
__defaults__: A tuple containing default argument values for those arguments that have defaults, or None if no arguments have a default value.
and:
co_argcount is the number of positional arguments (including arguments with default values);
You could do that by using the inspect module like,
>>> import inspect
>>> def foo(a, b, *, c=None):
... print(a, b, c)
...
>>> sig = inspect.signature(foo)
>>> sum(1 for param in sig.parameters.values() if param.kind == param.POSITIONAL_OR_KEYWORD)
2
The caveat is,
Python has no explicit syntax for defining positional-only parameters,
but many built-in and extension module functions (especially those
that accept only one or two parameters) accept them.

Inspect if an argument was passed positionally or via keyword

Consider a function with signature f(a, b). In future, I would like to change the signature to f(a, *, b), disallowing b to be passed as positional argument.
To reduce the impact of the change, I want to first deprecate specifying b positionally, warning users that do so.
For that I would like to write something like:
def f(a, b):
frame = inspect.currentframe()
if b in frame.specified_as_positional:
print('Do not do that')
else:
print('Good')
The result would be that
>>> f(1, 2)
'Do not do that'
>>> f(1, b=2)
'Good'
inspect.getargvalues(frame) does not seem to be sufficient. The ArgInfo object just provides
>>> f(1,b=2)
ArgInfo(args=['a', 'b'], varargs=None, keywords=None, locals={'a': 1, 'b': 2})
Is such inspection even possible in Python? Conceptually the interpreter does not seem to be required to remember if a argument was specified positionally or as keyword.
Python 2 support would be nice to have but is not strictly required.
You can use a wrapper to add an extra step between the user and the function. In that step, you can examine the arguments before the names matter. Note that this depends on the fact that b doesn't have a default value and always must be given as an arg.
functools.wraps is used to make the decorated function resemble the original in a bunch of ways.
import functools
import warnings
def deprecate_positional(fun):
#functools.wraps(fun)
def wrapper(*args, **kwargs):
if 'b' not in kwargs:
warnings.warn(
'b will soon be keyword-only',
DeprecationWarning,
stacklevel=2
)
return fun(*args, **kwargs)
return wrapper
#deprecate_positional
def f(a, b):
return a + b
>>> f(1, b=2)
3
>>> f(1, 2)
Warning (from warnings module):
File "C:/Users/nwerth/Desktop/deprecate_positional.py", line 36
print(f(1, 2))
DeprecationWarning: b will soon be keyword-only
3
Here is a pretty hacky solution:
def f(a, c=None, b=None):
if c is not None:
print("do not do that")
else:
print("good")
where input f(1, b=2) prints good and f(1, 2) prints do not do that

TypeError: fun() takes at least 2 arguments (3 given)

This is my code:
def fun(x, y, b=None, c=None):
print(x,' ',y,' ',b,' ',c)
I am calling it as fun(1, b=2, c=4) and getting error TypeError: fun() takes at least 2 arguments (3 given).
I know this error is because of incorrect number of positional and keyword arguments.
Instead of this, I want whenever I call my function with incorrect no. of arguments, it should tell me which argument is provided.
For example: for above case it should say something like "argument y is nor provided".
Can I write a decorator for this purpose?
fun as defined need to get between 2 and 4 argument, as it has two mandatory arguments and two optional arguments. You did not provide one of the two mandatory ones:
fun(1, b=2, c=4) # What about the argument y?
You need to call it using one of the next forms:
fun(1, 2)
fun(1, 2, b=3)
fun(1, 2, c=4)
fun(1, 2, b=3, c=4)
If you want notification about insufficient arguments, you can use args and kwargs:
def fun(*args, **kwargs):
if len(args) < 2:
print("Need at least two arguments!"); return
if len(args) > 2 or len(kwargs) > 2:
print("Too much arguments supplied!"); return
x, y = args
a, b = kwargs.get('a', None), kwargs.get('b', None)
print(x, y, a, b)
I want to handle this error and prompt error like it requires these (names) positional arguments. Is it possible to write a decorator for this?
I did a bit of research and came across the inspect module. Perhaps something along these lines will suffice? Right now I'm catching TypeError and printing a message, but you may prefer throwing a new TypeError that contains the message instead.
import inspect
from functools import wraps
def inspect_signature(f):
signature = inspect.signature(f)
#wraps(f)
def decorator(*args, **kwargs):
try:
f(*args, **kwargs)
except TypeError:
print('Failed to call "{}" with signature {}. Provided args={} and kwargs={}.'.format(
f.__name__, signature, args, kwargs))
return decorator
#inspect_signature
def func(foo, bar):
print('Called successfully with foo={}, bar={}'.format(foo, bar))
pass
if __name__ == '__main__':
func(foo='a', bar='b')
func(foo='a')
func('a', 'b', 'c')
Output
Called successfully with foo=a, bar=b
Failed to call "func" with signature (foo, bar). Provided args=() and kwargs={'foo': 'a'}.
Failed to call "func" with signature (foo, bar). Provided args=('a', 'b', 'c') and kwargs={}.

Categories

Resources