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
Related
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}")
I want to pass a function, f(a=1,b=2) into g and use the 'a' value in g
def f(a,b): pass
def g(f): #print f.a
g(f(1,2)) should result in an output of 1
I looked into the inspect module but can't seem to get hold of f in g
This is as far as my programming knowledge has got me :
def g(f):
print(list(inspect.signature(f).parameters.keys()))
g(f(1,2)) results in: TypeError: None is not a callable object
This is not possible. When you call g(f(1,2)), f(1,2) is finished before g runs. The only way this would be possible would be for f to return its arguments.
You need to call g appropriately, so the f, as a function, is an argument:
g(f, 1, 2)
Now work that into your function:
def g(f, *args):
print(list(inspect.signature(f).parameters.keys()))
... and from here, you can iterate through args to make the proper call to f.
Does that get you moving?
You could do something like this:
def f(a, b):
print(a)
print(b)
Then define wrapper as:
def wrapper(func, a, b):
func(a, b)
Or if you want more flexibility use *args:
def wrapper(func, *args):
func(*args)
That flexibility comes with some risk if the number of arguments don't match. Which means you'll need to take care that all func passed to wrapper have a consistent signature.
You could use **kwargs which would help with the above, then:
def wrapper(func, **kwargs):
func(**kwargs)
Then calls to wrapper would look like:
wrapper(f, a=1, b=2)
This would allow for more flexibility and signature of func could vary as needed.
You could turn wrapper into a decorator but that's another question.
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.
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.
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={}.