zakmck
zakmck

Reputation: 3004

Understanding function arguments in Python

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' ] ) )

Upvotes: 1

Views: 995

Answers (1)

Tadhg McDonald-Jensen
Tadhg McDonald-Jensen

Reputation: 21453

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

Upvotes: 3

Related Questions