Is it possible to adapt this approach of using dictionaries to find/evaluate one of many functions and its corresponding args?

Viewed 57

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 Answers

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

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.

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.

Related