Clean up long list of keyword args in Python?

If I have a function

def func(a=1, b=2, c=3, d=4, e=5, f=6, g=7):
    return a * b * c * d * e * f * g

and I want extend this to have the entire alphabet as keyword arguments, is there a way to do that without having the arguments cause the line with def wrap through multiple lines while also not using the black box of **kwargs? In other words, I'd like a way to generate a list or dict of kwargs and their defaults that still tells the function explicitly what kwargs are allowed.


Solution 1:

My understanding of the use-case is that a function needs to be written that takes keywords which are named after every letter of the alphabet (I assume English/ASCII) - i.e., a-z

There is a need for validation of the argument(s) being passed.

The code should be concise.

Here's an idea based on the OP's code fragment...

from string import ascii_lowercase
import inspect

def func(d):
    for k in d.keys():
        if k not in ascii_lowercase:
            raise TypeError(f"{inspect.stack()[0].function}() got an unexpected keyword argument '{k}'")
    r = 1
    for c in 'abcdefg':
        r *= d.get(c, 1)
    return r

d = {k: v for v, k in enumerate('abcdefg', 1)}

print(func(d))

func() takes a single argument to a dictionary. That dictionary contains keys which must be in the range a-z. Not all values are needed.

func() checks the keys to ensure that they are all part of the a-z range. If not, it simulates the same Exception that would arise if a named parameter was passed that had not been declared as valid/recognised.

func() wants to multiply the values assigned to a-g and return the result.

In the main flow of the program, a dictionary is created thus:

{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7}

The result, as expected is, 5040.

If the dictionary were to contain, for example,

{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'P':0}

...then the following would be emitted:

TypeError: func() got an unexpected keyword argument 'P'

Note:

Requires Python 3.5+

Solution 2:

You could write a (somewhat convoluted) decorator to provide the arguments and their default value as signature to the function:

import inspect
def signature(params): 
    arglist = ",".join(f"{k}={v.__repr__()}" for k,v in params.items())
    def addSignature(f):
        names = dict()
        code  = inspect.getsource(f).split("):\n")[-1]
        exec(f"def f({arglist}):\n"+code,names)
        return names['f']
    return addSignature

usage:

@signature(dict(zip("abcdefghijklmnopqrstuvwxyz",range(1,27))))
def myFunc():
    print(locals())

myFunc(4,5) # all arguments are available as local variables

{'a': 4, 'b': 5, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8, 'i': 9,
 'j': 10, 'k': 11, 'l': 12, 'm': 13, 'n': 14, 'o': 15, 'p': 16, 'q': 17,
 'r': 18, 's': 19, 't': 20, 'u': 21, 'v': 22, 'w': 23, 'x': 24, 'y': 25,
 'z': 26}

inspect.signature(myFunc)
<Signature (a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9, j=10, k=11, 
l=12, m=13, n=14, o=15, p=16, q=17, r=18, s=19, t=20, u=21, v=22, w=23, 
x=24, y=25, z=26)>

The decorator essentially rewrites the function with the signature assembled from string representations of the dictionary key/values.