27

For my own entertainment, I was wondering how to achieve the following:

functionA = make_fun(['paramA', 'paramB'])
functionB = make_fun(['arg1', 'arg2', 'arg3'])

equivalent to

def functionA(paramA, paramB):
    print(paramA)
    print(paramB)

def functionB(arg1, arg2, arg3):
    print(arg1)
    print(arg2)
    print(arg3) 

This means the following behaviour is required:

functionA(3, paramB=1)       # Works
functionA(3, 2, 1)           # Fails
functionB(0)                 # Fails

The focus of the question is on the variable argspec - I comfortable creating the function body using the usual decorator techniques.

For those that are interested, I was playing around with trying to programmatically create classes like the following. Again the difficulty is in generating the __init__ method with programmatic parameters - the rest of the class appears straightforward using a decorator or maybe a metaclass.

class MyClass:
    def __init__(self, paramA=None, paramB=None):
        self._attr = ['paramA', 'paramB']
        for a in self._attr:
            self.__setattr__(a, None)

    def __str__(self):
        return str({k:v for (k,v) in self.__dict__.items() if k in self._attributes})
Zero
  • 11,593
  • 9
  • 52
  • 70
  • 2
    possible duplicate of [Python: Can a variable number of arguments be passed to a function?](http://stackoverflow.com/questions/919680/python-can-a-variable-number-of-arguments-be-passed-to-a-function) or [Python Unpack Argument List for Format String](http://stackoverflow.com/questions/17075603/python-unpack-argument-list-for-format-string) – fredtantini Nov 18 '14 at 08:19
  • 2
    I'm not asking to pass a variable number of args. I want to programmatically create a function spec of any form. Decorators and metaclasses can be used to programmatically create functions and classes - but every example I've come across has the function specification hard coded. – Zero Nov 18 '14 at 22:02

3 Answers3

15

You can use exec to construct the function object from a string containing Python code:

def make_fun(parameters):
    exec("def f_make_fun({}): pass".format(', '.join(parameters)))
    return locals()['f_make_fun']

Example:

>>> f = make_fun(['a', 'b'])
>>> import inspect
>>> print(inspect.signature(f).parameters)
OrderedDict([('a', <Parameter at 0x1024297e0 'a'>), ('b', <Parameter at 0x102429948 'b'>)])

If you want more functionality (e.g., default argument values), it's a matter of adapting the string that contains the code and having it represent the desired function signature.

Disclaimer: as pointed out below it's important that you verify the contents of parameters and that the resulting Python code string is safe to pass to exec. You should construct parameters yourself or put restrictions in place to prevent the user from constructing a malicious value for parameters.

Simeon Visser
  • 118,920
  • 18
  • 185
  • 180
  • Nice, just keep in mind not to use it with user input, otherwise one could use it like this: `make_fun(['):\n import sys\n sys.exit()\n if (True'])()` and make your code blow up :P – Maciej Gol Nov 18 '14 at 22:54
  • @kroolik: yes, that's a good point. Added a disclaimer to be sure. It's indeed important to make sure the constructed code is safe for execution. – Simeon Visser Nov 18 '14 at 22:57
  • Accepted since this is the simplest solution - and the only sure way to get identical behaviour to defining the function manually. – Zero Nov 19 '14 at 23:26
6

One of the possible solutions using a class:

def make_fun(args_list):
    args_list = args_list[:]

    class MyFunc(object):
        def __call__(self, *args, **kwargs):
            if len(args) > len(args_list):
                raise ValueError('Too many arguments passed.')

            # At this point all positional arguments are fine.
            for arg in args_list[len(args):]:
                if arg not in kwargs:
                    raise ValueError('Missing value for argument {}.'.format(arg))

            # At this point, all arguments have been passed either as
            # positional or keyword.
            if len(args_list) - len(args) != len(kwargs):
                raise ValueError('Too many arguments passed.')

            for arg in args:
                print(arg)

            for arg in args_list[len(args):]:
                print(kwargs[arg])

    return MyFunc()

functionA = make_fun(['paramA', 'paramB'])
functionB = make_fun(['arg1', 'arg2', 'arg3'])

functionA(3, paramB=1)       # Works
try:
    functionA(3, 2, 1)           # Fails
except ValueError as e:
    print(e)

try:
    functionB(0)                 # Fails
except ValueError as e:
    print(e)

try:
    functionB(arg1=1, arg2=2, arg3=3, paramC=1)                 # Fails
except ValueError as e:
    print(e)
Maciej Gol
  • 15,394
  • 4
  • 33
  • 51
  • 1
    I like this as a general approach for processing *arg and **kwarg but IMO overcomplicates the use-case I am looking for. Although... constructing and executing a string does seem a little clunky! – Zero Nov 19 '14 at 23:29
5

Here's another way to do it using functools.wrap, which preserves signature and docstring, at least in python 3. The trick is to create the signature and documentation in dummy functions which never get called. Here are a couple of examples.

Basic example

import functools

def wrapper(f):
    @functools.wraps(f)
    def template(common_exposed_arg, *other_args, common_exposed_kwarg=None, **other_kwargs):
        print("\ninside template.")
        print("common_exposed_arg: ", common_exposed_arg, ", common_exposed_kwarg: ", common_exposed_kwarg)
        print("other_args: ", other_args, ",  other_kwargs: ", other_kwargs)
    return template

@wrapper
def exposed_func_1(common_exposed_arg, other_exposed_arg, common_exposed_kwarg=None):
    """exposed_func_1 docstring: this dummy function exposes the right signature"""
    print("this won't get printed")

@wrapper
def exposed_func_2(common_exposed_arg, common_exposed_kwarg=None, other_exposed_kwarg=None):
    """exposed_func_2 docstring"""
    pass

exposed_func_1(10, -1, common_exposed_kwarg='one')
exposed_func_2(20, common_exposed_kwarg='two', other_exposed_kwarg='done')
print("\n" + exposed_func_1.__name__)
print(exposed_func_1.__doc__)

And the result is:

>> inside template.
>> common_exposed_arg:  10 , common_exposed_kwarg:  one
>> other_args:  (-1,) ,  other_kwargs:  {}
>>  
>> inside template.
>> common_exposed_arg:  20 , common_exposed_kwarg:  two
>> other_args:  () ,  other_kwargs:  {'other_exposed_kwarg': 'done'}
>>  
>> exposed_func_1
>> exposed_func_1 docstring: this dummy function exposes the right signature

Calling inspect.signature(exposed_func_1).parameters returns the desired signature. Using inspect.getfullargspec(exposed_func_1), however, still returns the signature of template. At least if you put any arguments common to all functions you want to make in the definition of template, those will appear.

If this is a bad idea for some reason, please let me know!

More complicated example

And you can get much more complicated than this, by layering in more wrappers and defining more distinct behaviors in an inner function:

import functools

def wrapper(inner_func, outer_arg, outer_kwarg=None):
    def wrapped_func(f):
        @functools.wraps(f)
        def template(common_exposed_arg, *other_args, common_exposed_kwarg=None, **other_kwargs):
            print("\nstart of template.")
            print("outer_arg: ", outer_arg, " outer_kwarg: ", outer_kwarg)
            inner_arg = outer_arg * 10 + common_exposed_arg
            inner_func(inner_arg, *other_args, common_exposed_kwarg=common_exposed_kwarg, **other_kwargs)
            print("template done")
        return template
    return wrapped_func

# Build two examples.
def inner_fcn_1(hidden_arg, exposed_arg, common_exposed_kwarg=None):
    print("inner_fcn, hidden_arg: ", hidden_arg, ", exposed_arg: ", exposed_arg, ", common_exposed_kwarg: ", common_exposed_kwarg)

def inner_fcn_2(hidden_arg, common_exposed_kwarg=None, other_exposed_kwarg=None):
    print("inner_fcn_2, hidden_arg: ", hidden_arg, ", common_exposed_kwarg: ", common_exposed_kwarg, ", other_exposed_kwarg: ", other_exposed_kwarg)

@wrapper(inner_fcn_1, 1)
def exposed_function_1(common_exposed_arg, other_exposed_arg, common_exposed_kwarg=None):
    """exposed_function_1 docstring: this dummy function exposes the right signature """
    print("this won't get printed")

@wrapper(inner_fcn_2, 2, outer_kwarg="outer")
def exposed_function_2(common_exposed_arg, common_exposed_kwarg=None, other_exposed_kwarg=None):
    """ exposed_2 doc """
    pass

It's a bit verbose, but the point is that there is a lot of flexibility in where the dynamic inputs from you (the programmer) come in when using this to create functions, and so with where the exposed inputs (from user of the function) get used.

Adam S.
  • 323
  • 3
  • 10