如何将关键字参数添加到 Python 2.7 中的包装函数?

Posted

技术标签:

【中文标题】如何将关键字参数添加到 Python 2.7 中的包装函数?【英文标题】:How can I add keyword arguments to a wrapped function in Python 2.7? 【发布时间】:2017-04-20 05:23:40 【问题描述】:

我首先要强调的是,我已经广泛地搜索了网络和 Python 文档 + ***,并且没有设法找到这个问题的答案。我还要感谢任何花时间阅读本文的人。

正如标题所示,我正在用 Python 编写一个装饰器,我希望它为包装函数添加关键字参数(请注意:我知道如何为装饰器本身添加参数,这不是我要问的)。

这是我为 Python 3(特别是 Python 3.5)编写的一段代码的工作示例。它使用装饰器参数,将关键字参数添加到包装函数,并定义新函数并将其添加到包装函数。

from functools import wraps

def my_decorator(decorator_arg1=None, decorator_arg2=False):
    # Inside the wrapper maker

    def _decorator(func):
        # Do Something 1

        @wraps(func)
        def func_wrapper(
                *args,
                new_arg1=False,
                new_arg2=None,
                **kwds):
            # Inside the wrapping function
            # Calling the wrapped function
            if new_arg1:
                return func(*args, **kwds)
            else:
                # do something with new_arg2
                return func(*args, **kwds)

        def added_function():
            print("Do Something 2")

        func_wrapper.added_function = added_function
        return func_wrapper

    return _decorator

现在这个装饰器可以通过以下方式使用:

@my_decorator(decorator_arg1=4, decorator_arg2=True)
def foo(a, b):
    print("a=, b=".format(a,b))

def bar():
    foo(a=1, b=2, new_arg1=True, new_arg2=7)
    foo.added_function()

现在,虽然这适用于 Python 3.5(我假设适用于任何 3.x),但我还没有设法使其适用于 Python 2.7。我在第一行得到一个SyntaxError: invalid syntax,它试图为func_wrapper 定义一个新的关键字参数,这意味着在导入包含此代码的模块时声明new_arg1=False, 的行。

将新关键字移动到func_wrapper 的参数列表的开头解决了SyntaxError,但似乎与包装函数的签名不一致;我现在在调用foo(1, 2) 时收到错误TypeError: foo() takes exactly 2 arguments (0 given)。如果我像foo(a=1, b=2) 那样明确地分配参数,这个错误就会消失,但这显然还不够——不出所料,我的新关键字参数似乎“窃取”了发送到包装函数的前两个位置参数。这是 Python 3 所没有的。

我很想在这方面得到您的帮助。感谢您抽出宝贵时间阅读本文。

谢伊

【问题讨论】:

代码不能按原样运行:你需要导入functools,并且added_function中有一个语句(可能是print("add_function"))。 @RoryYorke 已编辑!谢谢,人! :) 我在同一时间问这个问题真有趣(比你晚了 20 分钟)。很好,在我完成提问之后和按下“发布您的问题”按钮之前,我重新查看了“可能已经有你答案的问题”。 【参考方案1】:

要向现有函数的签名添加参数,同时使该函数表现得像普通的 python 函数(正确的帮助、签名和 TypeError 在提供错误参数的情况下引发),您可以使用 makefun,我专门开发了它解决这个用例。

特别是 makefun 提供了 @wraps 的替代品,它有一个 new_sig 参数,您可以在其中指定新签名。以下是您的示例的编写方式:

try:  # python 3.3+
    from inspect import signature, Parameter
except ImportError:
    from funcsigs import signature, Parameter

from makefun import wraps, add_signature_parameters

def my_decorator(decorator_arg1=None, decorator_arg2=False):
    # Inside the wrapper maker

    def _decorator(func):
        # (1) capture the signature of the function to wrap ...
        func_sig = signature(func)
        # ... and modify it to add new optional parameters 'new_arg1' and 'new_arg2'.
        # (if they are optional that's where you provide their defaults)
        new_arg1 = Parameter('new_arg1', kind=Parameter.POSITIONAL_OR_KEYWORD, default=False)
        new_arg2 = Parameter('new_arg2', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None)
        new_sig = add_signature_parameters(func_sig, last=[new_arg1, new_arg2])

        # (2) create a wrapper with the new signature
        @wraps(func, new_sig=new_sig)
        def func_wrapper(*args, **kwds):
            # Inside the wrapping function

            # Pop the extra args (they will always be there, no need to provide default)
            new_arg1 = kwds.pop('new_arg1')
            new_arg2 = kwds.pop('new_arg2')
            
            # Calling the wrapped function
            if new_arg1:
                print("new_arg1 True branch; new_arg2 is ".format(new_arg2))
                return func(*args, **kwds)
            else:
                print("new_arg1 False branch; new_arg2 is ".format(new_arg2))
                # do something with new_arg2
                return func(*args, **kwds)

        # (3) add an attribute to the wrapper
        def added_function():
            # Do Something 2
            print('added_function')

        func_wrapper.added_function = added_function
        return func_wrapper

    return _decorator

@my_decorator(decorator_arg1=4, decorator_arg2=True)
def foo(a, b):
    """This is my foo function"""
    print("a=, b=".format(a,b))

foo(1, 2, True, 7)  # works, except if you use kind=Parameter.KEYWORD_ONLY above (py3 only)
foo(1, 2, new_arg1=True, new_arg2=7)
foo(a=3, b=4, new_arg1=False, new_arg2=42)
foo(new_arg2=-1,b=100,a='AAA')
foo(b=100,new_arg1=True,a='AAA')
foo.added_function()

help(foo)

效果如你所愿:

new_arg1 True branch; new_arg2 is 7
a=1, b=2
new_arg1 True branch; new_arg2 is 7
a=1, b=2
new_arg1 False branch; new_arg2 is 42
a=3, b=4
new_arg1 False branch; new_arg2 is -1
a=AAA, b=100
new_arg1 True branch; new_arg2 is None
a=AAA, b=100
added_function
Help on function foo in module <...>:

foo(a, b, new_arg1=False, new_arg2=None)
    This is my foo function

因此,您可以看到公开的签名符合预期,而您的用户看不到内部结构。请注意,您可以通过在新签名中设置 kind=Parameter.KEYWORD_ONLY 来将两个新参数设为“仅限关键字”,但您已经知道这在 python 2 中不起作用。

最后,您可能有兴趣使用decopatch 使您的装饰器代码更具可读性和健壮性以适应无括号用法。除其他外,它还支持非常适合您的情况的“平面”样式,因为它删除了一层嵌套:

from decopatch import function_decorator, DECORATED

@function_decorator
def my_decorator(decorator_arg1=None, decorator_arg2=False, func=DECORATED):

    # (1) capture the signature of the function to wrap ...
    func_sig = signature(func)
    # ... 

    # (2) create a wrapper with the new signature
    @wraps(func, new_sig=new_sig)
    def func_wrapper(*args, **kwds):
        # Inside the wrapping function
        ...  

    # (3) add an attribute to the wrapper
    def added_function():
        # Do Something 2
        print('added_function')

    func_wrapper.added_function = added_function
    return func_wrapper

(我也是这个的作者,因为厌倦了嵌套和无括号处理而创建它)

【讨论】:

这个方案很好,当函数签名也用的时候,不仅仅是参数值。对我来说,使用 FastAPI 就是这种情况,因为 FastAPI 使用端点参数将它们转换为请求参数。这样我就可以编写一个以编程方式添加参数的装饰器。谢谢!【参考方案2】:

如果您只将附加参数指定为关键字,则可以将它们从 kw 字典中取出(见下文)。如果您需要它们作为位置 AND 关键字参数,那么我认为您应该能够在原始函数上使用 inspect.getargspec,然后在 func_wrapper 中处理 args 和 kw。

以下代码在 Ubuntu 14.04 上使用 Python 2.7、3.4(Ubuntu 提供)和 3.5(来自 Continuum)进行了测试。

from functools import wraps

def my_decorator(decorator_arg1=None, decorator_arg2=False):
    # Inside the wrapper maker

    def _decorator(func):
        # Do Something 1
        @wraps(func)
        def func_wrapper(
                *args,
                **kwds):
            # new_arg1, new_arg2 *CANNOT* be positional args with this technique
            new_arg1 = kwds.pop('new_arg1',False)
            new_arg2 = kwds.pop('new_arg2',None)
            # Inside the wrapping function
            # Calling the wrapped function
            if new_arg1:
                print("new_arg1 True branch; new_arg2 is ".format(new_arg2))
                return func(*args, **kwds)
            else:
                print("new_arg1 False branch; new_arg2 is ".format(new_arg2))
                # do something with new_arg2
                return func(*args, **kwds)

        def added_function():
            # Do Something 2
            print('added_function')

        func_wrapper.added_function = added_function
        return func_wrapper

    return _decorator

@my_decorator(decorator_arg1=4, decorator_arg2=True)
def foo(a, b):
    print("a=, b=".format(a,b))

def bar():
    pass
    #foo(1,2,True,7) # won't work
    foo(1, 2, new_arg1=True, new_arg2=7)
    foo(a=3, b=4, new_arg1=False, new_arg2=42)
    foo(new_arg2=-1,b=100,a='AAA')
    foo(b=100,new_arg1=True,a='AAA')
    foo.added_function()

if __name__=='__main__':
    import sys
    sys.stdout.flush()
    bar()

输出是

new_arg1 True branch; new_arg2 is 7
a=1, b=2
new_arg1 False branch; new_arg2 is 42
a=3, b=4
new_arg1 False branch; new_arg2 is -1
a=AAA, b=100
new_arg1 True branch; new_arg2 is None
a=AAA, b=100
added_function

【讨论】:

这很有道理,谢谢!我对此的唯一问题是新的关键字参数是二等公民。您必须阅读代码才能知道它们在那里,并且它们是要在函数调用中使用的参数。在 Python 3 示例代码中,它们作为关键字参数的状态在代码中更具可读性和清晰性。这当然是 Python 2 的问题,而不是您的答案。 :P 顺便说一句,不 - 我 需要它们作为位置参数。 :) 添加的参数在 Python 3 中也不能用作位置参数:*args 将捕获所有位置参数,并且引入了您使用的语法来添加仅关键字参数:python.org/dev/peps/pep-3102跨度> @RoeeShenberg 是的,我知道。我从不想将它们用作位置参数。 Python 3 的方式更加清晰易读。这对代码的开发人员和维护人员来说更好,但对用户没有任何意义。

以上是关于如何将关键字参数添加到 Python 2.7 中的包装函数?的主要内容,如果未能解决你的问题,请参考以下文章

python如何将可选参数或关键字参数从一个函数传递到另一个函数?

python 2.7 上的错误“__init__() 得到了意外的关键字参数‘tcp_nodelay’”

如何将字符串列添加到python中的另一个字符串?

如何从 Python 3.2 降级到 2.7?

Pandas - Python 2.7:如何将时间序列索引转换为一天中的秒数?

如何将字符串中的数字转换为 python 2.7 中的完整函数?