1

I'm trying to understand the following about dealing with functions and their arguments:

def print_my_arg(func, *args, **kwargs):
    func(*args, **kwargs)
    if 'my_arg' in kwargs: 
        print('  my_arg = {}'.format(kwargs['my_arg']))

def foo(call_no, my_arg='Default Value'):
    print('call_no = {}'.format(call_no) )

print_my_arg(foo, 0, my_arg='My Value 1')
print_my_arg(foo, 1, 'My Value 2')
print_my_arg(foo, 2)

Output:

call_no = 0
  my_arg = My Value 1
call_no = 1 # I'd like to see 'My Value 2' here
call_no = 2 # I'd like to see 'Default Value' here

Obviously people are free to invoke functions in either of the ways shown above, which makes me wonder: why my_arg doesn't go to kwargs anyway? Isn't there a uniform way to access parameters by name (and not by position), which doesn't depend on the way the function was invoked?

Please note that:

  1. I'm not interested in print_my_args(func, call_no, my_arg), because I'm talking about the case where I don't know the signature of func in advance and yet I want to know if a particular parameter exists (by name).

  2. Clearly that's related to decorators, but I've written the example in a simpler way (or I hope so).

EDIT

Many thanks for the answers about inspect.signature. Using that, my new version of print_my_arg() is:

from inspect import signature

def print_my_arg ( func, *args, **kwargs ):
  func ( *args, **kwargs )
  sig = signature ( func )
  if 'my_arg' not in sig.parameters: return

  binding = sig.bind ( *args, **kwargs )
  binding.apply_defaults ()

  print ( "  my_arg = {}".format ( binding.arguments [ 'my_arg' ] ) )
zakmck
  • 2,715
  • 1
  • 37
  • 53
  • 1
    Positional arguments don't *"go to"* `**kwargs`, because *they're not keyword arguments*. If you want to pass the second positional argument to the function, you'll have to do that explicitly, it's not at all clear why you expected different behaviour. You could force keyword arguments if you really wanted to, see e.g. http://stackoverflow.com/a/37829651/3001761 – jonrsharpe Jun 19 '16 at 17:10
  • 2
    do you want to inspect the [`signature`](https://docs.python.org/3/library/inspect.html?highlight=signature#inspect.Signature.bind) of the function to check the argument name in `print_my_args`? then you could do `passed_args = inspect.signature(func).bind(*args,**kwargs)` then check if `my_arg` is in `passed_args`. – Tadhg McDonald-Jensen Jun 19 '16 at 17:13
  • http://stackoverflow.com/questions/3394835/args-and-kwargs – Julien Goupy Jun 19 '16 at 17:19

1 Answers1

3

Isn't there a uniform way to access parameters by name (and not by position), which doesn't depend on the way the function was invoked?

Yes by inspecting the signature:

>>> import inspect
>>> sig = inspect.signature(foo)
>>> print(sig)
(call_no, my_arg='Default Value')
>>> args = sig.bind(1, "my_value")
>>> args.arguments["my_arg"]
'my_value'

Note that trying to bind the signature to an invalid call will raise similar/same TypeError that would be raised when calling the function with invalid arguments. Also arguments that use the default will not be present in args.arguments unless you call args.apply_defaults()

Also note that keyword only arguments will be in args.kwargs dictionary instead of args.arguments:

import inspect

def bar(a,*, b=None):
    pass

sig = inspect.signature(bar)

binding = sig.bind(1, b=5)

assert "a" in binding.arguments
assert "b" in binding.kwargs
Tadhg McDonald-Jensen
  • 20,699
  • 5
  • 35
  • 59