12

Suppose I have this function:

def f(x,y):
   return x+y

If I use inspect.getargspec(f).args I get ['x','y'] as a result. Great.

Now suppose I want to create another function g(a,b) at runtime, where I don't know the argument names a and b until runtime:

def g(a,b):
   return f(a,b)

Is there a way to do this? Lambdas are almost right, except I can only assign argument names at compile time.

g = lambda *p: f(*p)

Somehow I want to create the function dynamically at run time based on a list L (for example L=['a','b']), so that inspect.getargspec(g).args == L).

martineau
  • 119,623
  • 25
  • 170
  • 301
Jason S
  • 184,598
  • 164
  • 608
  • 970
  • Not sure I understood the question. Guess you could create the `lambda` using an `eval` statement... Can you give an example of a function created at runtime? – tobias_k Dec 20 '13 at 21:48
  • You could use `FunctionType` and `CodeType` to construct the object, but I'm not sure I see the point. – DSM Dec 20 '13 at 22:11
  • 2
    This sounds like an XY problem - In the same was as dynamically named _variables_ normally mean the asker wants a list, this feels like the wrong route to solve a problem – Eric Dec 20 '13 at 22:35
  • 3
    I'm going to make a random guess and assume your intent is to _preserve_ argument names in a decorator such that a documentation tool using `inspect.getargspec` gives something useful - in that case, you want the [`decorator`](https://pypi.python.org/pypi/decorator) module – Eric Dec 20 '13 at 22:37
  • @Eric nice guess :) mine first was: "how to obfuscate function args from inspect" lolz – alko Dec 20 '13 at 22:55
  • @Eric: correct. I'm stuck with using someone else's code that relies on `inspect.getargspec`. – Jason S Dec 20 '13 at 23:05
  • @JasonS: Should I post that as an answer then? – Eric Dec 20 '13 at 23:35
  • Did any of these answers help? – Matt Williamson Dec 27 '13 at 16:39

4 Answers4

6

Here's a somewhat hacky way to do it which first creates a new function from an existing one with the modification and then replaces the original's code with it. It's lengthly mostly because the types.CodeType() call has so many arguments. The Python 3 version is somewhat different because a number of the function.func_code attributes were renamed and the calling sequence of types.CodeType() was changed slightly.

I got the idea from this answer by @aaronasterling (who says he got the idea from Michael Foord's Voidspace blog entry #583 titled Selfless Python). It could easily be made into a decorator, but I don't see that as being helpful based on what you've told us of the intended usage.

import sys
import types

def change_func_args(function, new_args):
    """ Create a new function with its arguments renamed to new_args. """

    if sys.version_info[0] < 3:  # Python 2?
        code_obj = function.func_code
        assert(0 <= len(new_args) <= code_obj.co_argcount)
        # The arguments are just the first co_argcount co_varnames.
        # Rreplace them with the new argument names in new_args.
        new_varnames = tuple(new_args[:code_obj.co_argcount] +
                             list(code_obj.co_varnames[code_obj.co_argcount:]))
        new_code_obj = types.CodeType(code_obj.co_argcount,
                                      code_obj.co_nlocals,
                                      code_obj.co_stacksize,
                                      code_obj.co_flags,
                                      code_obj.co_code,
                                      code_obj.co_consts,
                                      code_obj.co_names,
                                      new_varnames,
                                      code_obj.co_filename,
                                      code_obj.co_name,
                                      code_obj.co_firstlineno,
                                      code_obj.co_lnotab,
                                      code_obj.co_freevars,
                                      code_obj.co_cellvars)
        modified = types.FunctionType(new_code_obj, function.func_globals)

    else:  # Python 3
        code_obj = function.__code__
        assert(0 <= len(new_args) <= code_obj.co_argcount)
        # The arguments are just the first co_argcount co_varnames.
        # Replace them with the new argument names in new_args.
        new_varnames = tuple(new_args[:code_obj.co_argcount] +
                             list(code_obj.co_varnames[code_obj.co_argcount:]))

        new_code_obj = types.CodeType(code_obj.co_argcount,
                                      code_obj.co_posonlyargcount,
                                      code_obj.co_kwonlyargcount,
                                      code_obj.co_nlocals,
                                      code_obj.co_stacksize,
                                      code_obj.co_flags,
                                      code_obj.co_code,
                                      code_obj.co_consts,
                                      code_obj.co_names,
                                      new_varnames,
                                      code_obj.co_filename,
                                      code_obj.co_name,
                                      code_obj.co_firstlineno,
                                      code_obj.co_lnotab)

        modified = types.FunctionType(new_code_obj, function.__globals__)

    function.__code__ = modified.__code__  # replace code portion of original

if __name__ == '__main__':

    import inspect

    def f(x, y):
        return x+y

    def g(a, b):
        return f(a, b)

    print('Before:')
    print('inspect.getargspec(g).args: {}'.format(inspect.getargspec(g).args))
    print('g(1, 2): {}'.format(g(1, 2)))

    change_func_args(g, ['p', 'q'])

    print('')
    print('After:')
    print('inspect.getargspec(g).args: {}'.format(inspect.getargspec(g).args))
    print('g(1, 2): {}'.format(g(1, 2)))
martineau
  • 119,623
  • 25
  • 170
  • 301
2

I have a feeling you want something like this:

import inspect
import math

def multiply(x, y):
    return x * y

def add(a, b):
    return a + b

def cube(x):
    return x**3

def pythagorean_theorum(a, b, c):
    return math.sqrt(a**2 + b**2 + c**2)

def rpc_command(fname, *args, **kwargs):
    # Get function by name
    f = globals().get(fname)
    # Make sure function exists
    if not f:
        raise NotImplementedError("function not found: %s" % fname)
    # Make a dict of argname: argvalue
    arg_names = inspect.getargspec(f).args
    f_kwargs = dict(zip(arg_names, args))
    # Add kwargs to the function's kwargs
    f_kwargs.update(kwargs)
    return f(**f_kwargs)

Usage:

>>> # Positional args
... rpc_command('add', 1, 2)
3
>>> 
>>> # Keyword args
... rpc_command('multiply', x=20, y=6)
120
>>> # Keyword args passed as kwargs
... rpc_command('add', **{"a": 1, "b": 2})
3
>>> 
>>> # Mixed args
... rpc_command('multiply', 5, y=6)
30
>>> 
>>> # Different arg lengths
... rpc_command('cube', 3)
27
>>> 
>>> # Pass in a last as positional args
... rpc_command('pythagorean_theorum', *[1, 2, 3])
3.7416573867739413
>>> 
>>> # Try a non-existent function
... rpc_command('doesntexist', 5, 6)
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 6, in rpc_command
NotImplementedError: function not found: doesntexist
Matt Williamson
  • 39,165
  • 10
  • 64
  • 72
0

How about using keyword arguments?

>>> g = lambda **kwargs: kwargs
>>> g(x=1, y=2)
{'y': 2, 'x': 1}
>>> g(a='a', b='b')
{'a': 'a', 'b': 'b'}

Something like:

g = lambda **kwargs: f(kwargs.get('a', 0), kwargs['b'])

or let's say you want to use just the values:

>>> g = lambda **kwargs: f(*kwargs.values())
>>> def f(*args): print sum(args)
... 
>>> g(a=1, b=2, c=3)
6

In any case, using the **kwargs syntax results in kwargs being a dictionary of all the arguments passed by name.

Matt Williamson
  • 39,165
  • 10
  • 64
  • 72
  • 2
    The problem with `**kwargs` is that the resulting dictionary's keys aren't always going to be in the order you passed them in. – Blender Dec 20 '13 at 21:59
  • The values being in arbitrary order means your second example could easily pass them to `f()` in the wrong order, which most of the time matters (although it doesn't with the function shown). – martineau Dec 21 '13 at 15:38
0

You may use *args and **kwargs

let say you generate a dynamic function at runtime

def func():
    def dyn_func(*args, **kwargs):
        print args, kwargs
  return dyn_func

it is then possible to use args into your generated function

f = func()
f(test=1)

would give:

() {'test': 1}

then it is possible to manage args as you wish

Neuron
  • 5,141
  • 5
  • 38
  • 59
utopMan
  • 11