2

Suppose one has several separate functions to evaluate some given data. Rather than use redundant if/else loops, one decides to use a dictionary key to find the particular function and its corresponding args. I feel like this is possible, but I can't figure out how to make this work. As a simplified example (that I hope to adapt for my case), consider the code below:

def func_one(x, a, b, c=0):
    """ arbitrary function """
    # c is initialized since it is necessary in func_two and has no effect in func_one
    return a*x + b

def func_two(x, a, b, c):
    """ arbitrary function """
    return a*x**2 + b*x + c

def pick_function(key, x=5):
    """ picks and evaluates arbitrary function by key """
    if key != (1 or 2):
        raise ValueError("key = 1 or 2")

    ## args = a, b, c
    args_one = (1, 2, 3)
    args_two = (4, 5, 3)

    ## function dictionary
    func_dict = dict(zip([1, 2], [func_one, func_two]))

    ## args dictionary
    args_dict = dict(zip([1, 2], [args_one, args_two]))

    ## apply function to args
    func = func_dict[key]
    args = args_dict[key]

    ## my original attempt >> return func(x, args)
    return func(x, *args) ## << EDITED SOLUTION VIA COMMENTS BELOW

print(func_one(x=5, a=1, b=2, c=3)) # prints 7

But,

print(pick_function(1)) 

returns an error message

  File "stack_overflow_example_question.py", line 17, in pick_function
    return func(x, args)
TypeError: func_one() missing 1 required positional argument: 'b'

Clearly, not all of the args are being passed through with the dictionary. I've tried various combinations of adding/removing extra brackets and paranthesis from args_one and args_two (as defined in pick_function). Is this approach fruitful? Are there other convenient (in terms of readability and speed) approaches that do not require many if/else loops?

3 Answers3

2

To fix your code with minimal changes, change return func(x, args) to return func(x, *args). I think this is what Anton vBR is suggesting in the comments.


However, I think your code could be further simplified by using the * ("splat") and ** ("double-splat"?) positional/keyword argument unpacking operators like this:

def func_one(x, a, b, c=0):
    """ arbitrary function """
    # c is initialized since it is necessary in func_two and has no effect in func_one
    return a*x + b

def func_two(x, a, b, c):
    """ arbitrary function """
    return a*x**2 + b*x + c

def func(key, *args, **kwargs):
    funcmap = {1: func_one, 2: func_two}
    return funcmap[key](*args, **kwargs)

def pick_function(key, x=5):
    """ picks and evaluates arbitrary function by key """
    argmap = {1: (1, 2, 3), 2: (4, 5, 3)}
    return func(key, x, *argmap[key])

print(func_one(x=5, a=1, b=2, c=3)) 
# 7
print(pick_function(1)) 
# 7
print(func(1, 5, 1, 2, 3)) 
# 7
print(func(1, b=2, a=1, c=3, x=5)) 
# 7
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • This seems to be the simplest solution of those posted. I will ask some related follow-up questions tomorrow if you don't mind. –  Oct 07 '17 at 11:05
1

If I understand what you are asking, I don't know that you want to use 1, 2... as your dictionary keys anyway. You can pass the name of the function and the list of the args you want to use straight to a function and use it that way like:

def use_function(func, argList):
    return (func(*argList))

print(use_function(func_one, [5, 1, 2, 3]))

or:

def use_function_2(func, argDict):
    return (func(**argDict))

print(use_function_2(func_one, {'a':1, 'b':2,'x':5, 'c':3}))

And if you like you could still use a dictionary to hold numbers which correspond to functions as well. That would look like this:

def pick_function_2(key, x=5):
    """ picks and evaluates arbitrary function by key """
    if key != (1 or 2):
        raise ValueError("key = 1 or 2")

    ## args = a, b, c
    args_one = [1, 2, 3]
    args_two = [4, 5, 3]

    ## function dictionary
    func_dict = dict(zip([1, 2], [func_one, func_two]))

    ## args dictionary
    args_dict = dict(zip([1, 2], [args_one, args_two]))

    ## apply function to args
    func = func_dict[key]
    args = args_dict[key]

    return func(*([x] + args))

print(pick_function_2(1))

However, this is starting to get a bit confusing. I would make sure you take some time and just double check that this is actually what you want to do.

wsad597
  • 53
  • 8
  • I can try this in about an hour and play around with it, thanks. I am trying this approach because I am comparing multiple datasets with the same minimizable error metric functions (ex: chi square and mle), each with different inputtable args. Though I'm always curious about alternative methods. –  Oct 07 '17 at 10:19
  • If I use the splat operator as you did in the last example, then it covers the args sequentially, correct? If so, does this mean that all (non-initialized?) inputs of the called function (func-one or func_two) must be in args? –  Oct 07 '17 at 10:47
  • 1
    yes, the * operator converts the args sequentially. The ** operator converts them based on keyword so the order doesn't matter. If you know the names of the args ahead of time you can use **, other wise, yes, you would need to put them in *args and unpack them that way. @darioSka seems to have a good answer if that is what you want to do. or if you want to mix it up. – wsad597 Oct 07 '17 at 11:55
  • 1
    Also, here is a good answer which explains the * and ** notation: https://stackoverflow.com/a/2921893/6357143 – wsad597 Oct 07 '17 at 12:01
1

You are trying to mixing named arguments and unnamed arguments!

As rule of thumb, if you pass a dict to a function using ** notation, you can access the arguments using **kwargs value. However, if you pass a tuple to the funcion using * notation, you can access the arguments using *args value.

I edited the method in the following way:

 def func_one(*args, **kwargs):
    """ arbitrary function """
    # c is initialized since it is necessary in func_two and has no effect in func_one
    if args:
        print("args: {}".format(args))
        x, other_args, *_ = args
        a, b , c = other_args
    elif kwargs:
        print("kwargs: {}".format(kwargs))
        x, a , b , c = kwargs['x'], kwargs['a'], kwargs['b'], kwargs['c']
    return a*x + b

So, in the first call you have:

print(func_one(x=5, a=1, b=2, c=3)) # prints 7
kwargs: {'b': 2, 'a': 1, 'x': 5, 'c': 3}
7

Because you are passing named arguments.

In the second execution, you'll have:

print(pick_function(1)) 
args: (5, (1, 2, 3))
7

I know you wanted to find a solution without if/else, but you have to discriminate between theese two cases.

dariodip
  • 230
  • 3
  • 13
  • I need to understand the difference btwn args and kwargs. This was illuminating, thanks. –  Oct 07 '17 at 10:32
  • So if I want some args and not others (ex: c is not an allowed input in function_one), then kwargs are the way to go (since they are basically named variables/args). Is that correct? –  Oct 07 '17 at 10:42
  • @mikey Yes, it's the safest way to go – dariodip Oct 07 '17 at 11:35