简体   繁体   English

如何以与原始函数可见的相同顺序处理包装函数参数?

[英]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 ).我试图在调用之前检查函数参数的类型(在这个例子中是foo )。 I am using python decorators to achieve this.我正在使用 python 装饰器来实现这一点。 I don't see how I can get arguments in the same order as they are visible to the function foo .我看不到如何以与函数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:类型检查有点弱,只要您注释代码,注释就可以工作,但是可以通过使用标准库中的inspect来实现更健壮的方法:

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.在这种情况下, inspect.signature可用于获取原始函数的签名以获取参数的原始顺序。 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:该代码与annotations兼容:

@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.它是上述代码的副本,但使用__annotations__属性来获取签名。 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).所以在kwargs上执行任何特定的命令没有多大意义(或者至少会让任何试图使用你的装饰器的人感到困惑)。 So you will probably want to check for which kwargs are specified and remove the corresponding argument types.因此,您可能需要检查指定了哪些kwargs并删除相应的参数类型。

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 代码中可能在很大程度上是不需要和不必要的。 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. Python 被设计为动态类型,因此任何类似于 Python 中的强类型的东西都违背了规律,大多数人都不会期望。

Next, since python 3.5 we have had access to the built-in typing package.接下来,从 python 3.5 开始,我们可以访问内置的typing包。 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.现在这实际上不会为您做任何类型断言,但它会让您清楚地知道您期望什么类型,并且大多数(如果不是全部)IDE 会给您视觉警告,表明您将错误的类型传递给函数。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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