5

If I have a Python function defined as f(a, b, c), is there a straightforward way to get a dictionary mapping the formal names to the values passed in? That is, from inside f, I'd like to be able to get a dictionary {'a': 1, 'b': 2, 'c': 3} for the call f(1, 2, 3).

I'd like to do this so I can directly use the arguments and values in a string substitution, e.g. "%(a)s %(b)s %(c)s" % d. I could just do something like d = dict(a=a, b=b, c=c), but I'd rather avoid the repetition if possible.

I know this is quite easy to do by defining f as f(**kwds), but that also makes it less obvious what arguments the function expects. It looks like there'd probably be some way to do this via the inspect module, but it'd probably be too complex to be worthwhile.

I suspect there's no good answer to this question as posed, but I'd be happy to be proven wrong. Alternative approaches for accomplishing what I described above would be welcome too.

user2864740
  • 60,010
  • 15
  • 145
  • 220
jjlin
  • 4,462
  • 1
  • 30
  • 23
  • Would function(*args) do the job? – Vincent Audebert Mar 13 '14 at 20:45
  • 1
    There is [`locals()`](http://docs.python.org/2.7/library/functions.html#locals), although it's not strictly limited. – user2864740 Mar 13 '14 at 20:46
  • 1
    What you're doing may cause you problems: http://stackoverflow.com/questions/1550479/python-is-using-vars-locals-a-good-practice It's not quite as bad as using `locals`, but it couples the function signature and argument names to the format string in a way you'll regret. – user2357112 Mar 13 '14 at 21:15

4 Answers4

7
>>> import inspect
>>> def foo(a,b,c):
...     pass
... 
>>> inspect.getargspec(foo)
ArgSpec(args=['a', 'b', 'c'], varargs=None, keywords=None, defaults=None)
>>> inspect.getargspec(foo).args
['a', 'b', 'c']

Now you can filter locals() based on those.

>>> x = 'outer'
>>> G = 'GLOBAL'
>>> def foo(a, b, c=1, *splat, **splatsplat):
...     global G
...     d = 'inner'
...     b = 'stomped'
...     print(locals())
...     print(inspect.getargspec(foo))
...     
>>> foo(1, 2, 3, 'splat1', 'splat2', named='potato')
{'splatsplat': {'named': 'potato'}, 'splat': ('splat1', 'splat2'), 'd': 'inner', 'b': 'stomped', 'c': 3, 'a': 1}
ArgSpec(args=['a', 'b', 'c'], varargs='splat', keywords='splatsplat', defaults=(1,))

The only thing I don't know how to get is to recover the original value of b; I think that's probably impossible.

wim
  • 338,267
  • 99
  • 616
  • 750
  • Only problem here is that OP wanted to do it from within the function. This does not fulfil that criteria. – anon582847382 Mar 13 '14 at 20:58
  • 1
    Yes it does. You can do it from within the function! – wim Mar 13 '14 at 21:00
  • Sorry, just read the documentation for `getargspec`. I skimmed over your answer too quickly and saw that it was not within the function and thought it was not quite right. My apologies, never would have thought of this. A very dynamic solution. – anon582847382 Mar 13 '14 at 21:02
  • Then why didn't you show its use inside a function? – martineau Mar 13 '14 at 21:07
  • Sorry, I thought it was obvious. I will add it now. – wim Mar 13 '14 at 21:11
  • I was looking at `getargvalues(currentframe(0))` which gives `ArgInfo(args=['a', 'b', 'c'], varargs=None, keywords=None, locals={'a': 1, 'c': 3, 'b': 2})` for `f(1, 2, 3)` for instance... However, amending any of those variables inside the function before doing the check will give you the modified value - not the values in the original call - which is what I believe the OP is after. – Jon Clements Mar 13 '14 at 21:11
  • I was mulling over something like this, but one aspect I didn't really like about `inspect`-based solutions is you seem to have to provide the function name, which is quasi-needless repetition, and inconvenient if I decide to change the function name. Nevertheless, +1 for a more concrete solution (not quite complete without the filtering step). – jjlin Mar 13 '14 at 21:22
  • @jjlin see the comment above yours for digging the function name out dynamically – wim Mar 13 '14 at 21:32
  • @wim Ah, didn't notice that. Might consider adding that to the answer for anyone not wading through the comments in detail. Thanks to Jon Clements also. – jjlin Mar 13 '14 at 21:33
  • Just calling `getargspec()` doesn't provide a dictionary "mapping the formal names to the values passed in". A dictionary like that is needed for the string interpolation the OP wants to do. Since it's OK to have extra stuff in the dictionary, `locals()` is the simplest solution. – martineau Mar 16 '14 at 17:15
3

You can use locals() for the current scope:

>>> def f(a, b, c):
...     param_values = locals()  # Line must be very first in function.
...     print(param_values)

>>> f(1, 2, 3)
{'a': 1, 'b': 2, 'c': 3}

Conveniently, this seems to be the exact output that you want. In my opinion, this is the best way to do it from inside the function as you specified.

anon582847382
  • 19,907
  • 5
  • 54
  • 57
  • That's fine, but if you define any variable within the function before the print, it will end up in the dictionary. – msvalkon Mar 13 '14 at 20:51
  • @msvalkon Very good point. Modified so that the `print` statement can go anywhere. – anon582847382 Mar 13 '14 at 20:54
  • This requires you to already know which keys were the args, so it's not truly dynamic. – wim Mar 13 '14 at 20:56
  • I initially had it in my mind that filtering out the other locals would be problematic, but on further reflection, I think I can live with saving things off at the beginning. – jjlin Mar 13 '14 at 21:23
  • 2
    @jjlin: Be careful with closure variables. `locals` includes those. – user2357112 Mar 13 '14 at 21:29
  • Thanks, that's a good point, not relevant for my current situation, but definitely something that would point towards a more complex `inspect`-based solution. – jjlin Mar 13 '14 at 21:31
1

You could do it with two functions, one with a nice interface and the other with the implementation:

def f(a, b, c):
    """Docstring"""
    return _f(a, b, c)

def _f(**kwargs):
    ...
RemcoGerlich
  • 30,470
  • 6
  • 61
  • 79
0

You could avoid even more repetition by instead creating a generic function that does the string substitution as well. The following works in both Python 2.7.6 and 3.3.4:

import inspect
import sys

def print_func_args(call_stack_depth=1):
    frame = sys._getframe(call_stack_depth)
    funcname = inspect.getframeinfo(frame).function
    func = frame.f_globals[funcname]
    f_locals = frame.f_locals
    args = inspect.getargspec(func).args
    print(', '.join(('{}={}'.format(arg, f_locals[arg]) for arg in args)))

def foo(a, b, c):
    local_var = 42
    print_func_args()

foo(1, 2, 3)  # -> a=1, b=2, c=3
martineau
  • 119,623
  • 25
  • 170
  • 301
  • I was looking for a solution that would be easy to use in any random function, and 3 somewhat-involved lines of code isn't my idea of that. Can that code be moved out into another function, like `get_local_args_dict()` or something? The main issue would seem to be how to have that function pass `foo` or equivalent to `getargspec()`. – jjlin Mar 13 '14 at 21:44
  • Yes, it possible to move them to a separate function -- see updated answer. – martineau Mar 13 '14 at 21:59
  • I guess I wasn't that clear -- can it be done without explicitly passing the name `foo` from within `foo`? – jjlin Mar 13 '14 at 22:03
  • It's possible for a global function, but I haven't figured-out a truly general way so far -- unfortunately, I don't have much more time to work on this right now. – martineau Mar 13 '14 at 22:22
  • Thanks, this seems like a more general solution, and more technically interesting as well. I don't have time to look at it in detail now, but will consider updating the accepted answer later. – jjlin Mar 13 '14 at 22:56
  • I think it is better to get the function name with the help of `traceback.extract_stack`, since `sys._getframe` is "private" – wim Mar 13 '14 at 23:51
  • @wim: I don't really want the function name, just used it as a way to get to the function itself. WWIW, `inspect.currentframe()` uses `sys._getframe()` itself. It _is_ documented and I think has a leading underscore because it's not present in all implementations. One thing I don't like about using `traceback` is that you then have to start generating fake exceptions to get at information. – martineau Mar 14 '14 at 03:39