简体   繁体   中英

How can I type convert many arguments of a function in place?

Context

I use CherryPy to serve a simple webpage that shows different content based on the URL parameters. Specifically it takes the sum of the parameters and shows a different message based on that. In CherryPy webpages can be defined as functions, and URL parameters are passed as an argument to that function.
As explained in this tutorial URL parameters are passed as strings, so to calculate the sum I want to convert each argument to a float. I will have many URL parameters, so doing this one by one seems cumbersome.

How can I type convert (a large number of) arguments in place?

What I've tried

Dumb

The "dumb" approach would be to simply take each argument and re-assign it as a float:

def dumb(a="0", b="0", c="0", d="0", e="0", f="0", g="0"):
    a = float(a)
    b = float(b)
    c = float(c)
    d = float(d)
    e = float(e)
    f = float(f)
    g = float(g)

    return print(sum([a, b, c, d, e, f, g]))

It's readable, but rather repetitive and not very "pythonic".

Loop over locals()

Another option I found is to re-assign the locals to a dictionary, then loop over it and call the values from the dict.

def looping_dict(a="0", b="0", c="0", d="0", e="0", f="0", g="0"):
    args = locals()
    for key in args:
        if key in ["a", "b", "c", "d", "e", "f", "g"]:
            args[key] = float(args[key])

    return print(sum([args["a"], args["b"], args["c"], args["d"], args["e"], args["f"], args["g"]] ) )

This is a bit annoying as I have to reference the dictionary every time. So a simple reference d becomes args["d"] . Doesn't help code readability neither.

Here's a @convert decorator I've used before for something similar (originally inspired by https://stackoverflow.com/a/28268292/4597523 ):

import functools, inspect

def convert(*to_convert, to):
    def actual_convert(fn):
        arg_names = inspect.signature(fn).parameters.keys()
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            args_converted = [to(arg) if arg_name in to_convert else arg
                               for arg, arg_name in zip(args, arg_names)]
            kwargs_converted = {kw_name: to(val) if kw_name in to_convert else val
                                 for kw_name, val in kwargs.items()}
            return fn(*args_converted, **kwargs_converted)
        return wrapper
    return actual_convert

@convert('a', 'c', 'd', to=str)
def f(a, b, c=5, *, d, e=0):
    return a, b, c, d, e

print(f(1, 2, d=7))

# Output: ('1', 2, 5, '7', 0)
# Passed params `a` and `d` got changed to `str`,
# however `c` used the default value without conversion

It uses inspect.signature to get the non-keyword arg names. I am not sure how CherryPy passes the params or how it gets the names, but this might be a solid start. Using functools.wraps is important - it makes sure the original signature function signature is preserved, which seems to be important for CherryPy.

This is only documented in the changelog but since 2016 with cherrypy >= 6.2.0 there is a @cherrypy.tools.params tool doing exactly what you want (provided that you use a Python 3 version supporting type annotations):

import cherrypy


@cherrypy.tools.params()
def your_http_handler(
        a: float = 0, b: float = 0,
        c: float = 0, d: float = 0,
        e: float = 0, f: float = 0,
        g: float = 0,
):
    return str(a + b + c + d + e + f + g)

The PR that added it is PR #1442 — you can explore the usage by looking at the tests there.

If your Python is old for some reason, you could do:

import cherrypy


def your_http_handler(**kwargs):
    # Validate that the right query args are present in the HTTP request:
    if kwargs.keys() ^ {'a', 'b', 'c', 'd', 'e', 'f', 'g'}:
        raise cherrypy.HTTPError(400, message='Got invalid args!')

    numbers = (float(num) for num in kwargs.values())  # generator expression won't raise conversion errors here
    try:
        return str(sum(numbers))  # this will actually call those `float()` conversions so we need to catch a `ValueError`
    except ValueError as val_err:
        raise cherrypy.HTTPError(
            400,
            message='All args should be valid numbers: {exc!s}'.format(exc=val_err),
        )

PS In your initial post you use return print(...) which is wrong. print() always returns None so you'd be sending "None" back to the HTTP client while the argument of print(arg) would be just printed out in your terminal where you run the server.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM