简体   繁体   中英

How to process wrapped function arguments in the same order as visible for original function?

I am trying to examine the types of functions arguments before the call (in this example it is foo ). I am using python decorators to achieve this. I don't see how I can get arguments in the same order as they are visible to the function foo . In the following example, I get two different orderings but have essentially the same function call.

def wrapper(func):
    def f(*args, **kwargs):
        print([type(x) for x in args] + [type(v) for v in kwargs.values()])
        return func(*args, **kwargs)
    return f

@wrapper
def foo(a, b, c, d):
    print(f"{a} {b} {c} {d}")

foo(10, 12.5, 14, 5.2) # all good: int, float, int, float
foo(10, 12.5, d=5.2, c=14) # not what I want: int, float, float, int

Is it possible to get arguments in a consistent order? If not, then is it at least possible to get them all keyed by argument name? Something that looks like this:

def wrapper(func):
    def f(**kwargs):
        # kwargs = {'a': 10, 'b': 12.5, 'c': 14, 'd': 5.2}
        print([type(v) for v in kwargs.values()])
        return func(*args, **kwargs)
    return f

foo(10, 12.5, 14, 5.2) # obviously doesn't work

The type-checking is a bit weak, the annotations works as long you annotate your code but a more robust way can be achieved by using inspect from the standard library:

it provides full access to frame, ... and everything you may need. In this case with inspect.signature can be used to fetch the signature of the original function to get a the original order of the parameters. Then just regroup the parameters and pass the final group back to the original function. More details in the comments.

from inspect import signature

def wrapper(func):

    def f(*args, **kwargs):
        # signature object
        sign = signature(func)

        # use order of the signature of the function as reference
        order = order = dict.fromkeys(sign.parameters)

        # update first key-values
        order.update(**kwargs)

        # update by filling with positionals
        free_pars = (k for k, v in order.items() if v is None)
        order.update(zip(free_pars, args))

        return func(**order)
    return f

@wrapper
def foo(a, b, c, d):
    print(f"{a} {b} {c} {d}")


foo(10, 12.5, 14, 5.2)
#10 12.5 14 5.2
foo(10, 12.5, d=5.2, c=14)
#10 12.5 14 5.2

The code is annotations compatible:

@wrapper
def foo(a: int, b: float, c: int, d: float) -> None:
    print(f"{a} {b} {c} {d}")

The annotation's way, no imports required:

It is a copy past of the above code but using __annotations__ attribute to get the signature. Remember that annotations may or may not have an annotation for the output

def wrapper(func):

    def f(*args, **kwargs):

        if not func.__annotations__:
            raise Exception('No clue... inspect or annotate properly')

        params = func.__annotations__

        # set return flag
        return_has_annotation = False
        if 'return' in params:
            return_has_annotation = True

        # remove possible return value
        return_ = params.pop('return', None)

        order = dict.fromkeys(params)
        order.update(**kwargs)
        free_pars = (k for k, v in order.items() if v is None)
        order.update(zip(free_pars, args))

        # update with return annotation
        if return_has_annotation:
            func.__annotations__  = params | {'return': return_}

        return func(**order)

    return f

@wrapper
def foo(a: int, b: float, c: int, d: float) -> None:
    print(f"{a} {b} {c} {d}")

The first thing to be careful of is that key word arguments are implemented because order does not matter for them and are intended to map a value to a specific argument by name at call-time. So enforcing any specific order on kwargs does not make much sense (or at least would be confusing to anyone trying to use your decorater). So you will probably want to check for which kwargs are specified and remove the corresponding argument types.

Next if you want to be able to check the argument types you will need to provide a way to tell your decorator what types you are expected by passing it an argument (you can see more about this here ). The only way to do this is to pass a dictionary mapping each argument to the expected type:

@wrapper({'a': int, 'b': int, c: float, d: int})
def f(a, b, c=6.0, d=5):
    pass
def wrapper(types):
    def inner(func):
        def wrapped_func(*args, **kwargs):
            # be careful here, this only works if kwargs is ordered,
            # for python < 3.6 this portion will not work
            expected_types = [v for k, v in types.items() if k not in kwargs]
            actual_types = [type(arg) for arg in args]

            # substitute these in case you are dead set on checking for key word arguments as well
            # expected_types = types
            # actual_types = [type(arg) for arg in args)] + [type(v) for v in kwargs.items]

            if expected_types != actual_types:
                raise TypeError(f"bad argument types:\n\tE: {expected_types}\n\tA: {actual_types}")

            func(*args, **kwargs)
        return wrapped_func
    return inner


@wrapper({'a': int, 'b': float, 'c': int})
def f(a, b, c):
    print('good')

f(10, 2.0, 10)
f(10, 2.0, c=10)
f(10, c=10, b=2.0)

f(10, 2.0, 10.0) # will raise exception

Now after all of this, I want to point out that this is functionality is probably largely unwanted and unnecessary in python code. Python was designed to be dynamically typed so anything resembling strong types in python is going against the grain and won't be expected by most.

Next, since python 3.5 we have had access to the built-in typing package. This lets you specify the type that you expect to be receiving in a function call:

def f(a: int, b: float, c: int) -> int:
    return a + int(b) + c

Now this won't actually do any type assertions for you, but it will make it plainly obvious what types you are expecting, and most (if not all) IDEs will give you visual warnings that you are passing the wrong type to a function.

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