Python装饰器中参数传递的困惑

3
TL;DR: 如果函数装饰器没有明确指定参数,那么装饰后的函数与未装饰的函数在参数要求(参数数量和关键字参数名称)方面是否相同?
我有一个控制器用于处理购物车 AJAX 调用。在每次调用之初,它会初始化一个对象来处理实际的购物车功能逻辑。(这可能不是最有效的方法,但我的问题与实际的购物车部分无关。)
代码看起来像这样:
from webapp import request

from shopping_cart import Cart
from decorators import decorator

@decorator
def init_cart(f, *args, **kwargs):
    shopping_cart = Cart()
    return f(shopping_cart = cart)

@init_cart
def add_item(shopping_cart = None):
    shopping_cart.add(request.params)

@init_cart
def remove_item(shopping_cart = None):
    shopping_cart.remove(request.params)

等等。

这段代码存在于一个集中模块中,由多个应用程序导入和调用。各个应用程序中的代码如下:

from sharedlib.controllers import cart
from app.base import *

class CartController(BaseController):

    def index(self, url = None):
        set_content_type('text/javascript')
        controller_method = getattr(cart, url)
        if controller_method:
            return controller_method()
        else:
            abort(404)

我的问题如下:

如果我想要init_cart装饰器在传递cart模块的同时,将收到的参数传递给被调用的控制器方法,我应该这样尝试:

@decorator
def init_cart(f, *args, **kwargs):
    shopping_cart = Cart()
    kwargs['shopping_cart'] = cart
    return f(*args, **kwargs)

@init_cart
def add_item(shopping_cart = None):
    shopping_cart.add(request.params)

但是我遇到了一个异常,TypeError: add_item() got multiple values for keyword argument 'shopping_cart'

我并不完全理解这种行为:kwargs显然只有一个关于'shopping_cart'的键/值对。

此外,如果我尝试这样做:

@decorator
def init_cart(f, *args, **kwargs):
    shopping_cart = Cart()
    return f(cart)

@init_cart
def add_item(shopping_cart = None):
    shopping_cart.add(request.params)

我遇到了错误 TypeError: add_item() takes exactly 1 argument (0 given)

我认为我可能没有理解装饰器在接收和传递参数时的行为方式 - 我假设如果 init_cart 使用了 (*args, **kwargs),那么被它修饰的函数可以使用任何一组参数进行调用,甚至没有参数。但事实似乎并非如此。

除此之外,在之前的例子中,方法如何接收多个值作为 shopping_cart 参数呢?


如果Cart返回一个元组或其他类型的数据,那么它可能会出现错误吗? - Joran Beasley
Cart 是一个被初始化的类。想法是使用 shopping_cart 变量来传递在每个请求中使用的 Cart 实例。 - Michael Schuller
2个回答

1
def this_is_a_decorator(fn):
    def decorated_fn(*args,**kwargs):
        result = fn(*args,**kwargs) #notice we are calling the original function
        return "%s decorated"%result
    return decorated_fn

@this_is_a_decorator
def reverse_string(msg=""):
    return msg[::-1]

print reverse_string("Hello!")

给你的装饰器

def init_cart(fn):
    def decorated_fn(*args,**kwargs):
        cart = Cart()
        fn(cart)
        return cart
    return decorated_fn

@init_cart
def add_item(shopping_cart=None):
     shopping_cart.add(request.params)  

虽然我不想刁难你,但你没有提供比Python文档中可用的更多信息。我认为我大致了解装饰器的工作方式(你的代码证明了这一点),这就是为什么我感到困惑的原因;除非我漏掉了什么非常明显的东西,否则我认为装饰器对传递的函数的调用与我上面的每个示例中传递的函数的argspec匹配,但后两个示例都会抛出异常。这就是我无法理解的地方。 - Michael Schuller
我的init_cart示例应该可以满足你的需求...我不知道装饰器decorator在做什么...显然,你缺乏对装饰器通常工作原理的理解,因为你的装饰器不起作用,并且你的问题没有清楚地表明你对装饰器的一般理解,这就是为什么我提供了一个非常简单的装饰器示例。 - Joran Beasley
我觉得我可能显得有点好斗,所以请原谅。我试图弄清楚的问题可能与decorator模块的装饰器工厂函数的工作方式有关 - 你说得对,你的示例确实可以按原样工作。 - Michael Schuller

0

我发现Joran在他的回答中部分正确:虽然我手写装饰器的理解并不是错误的,但我完全没有理解使用decorator.decorator装饰器来简化创建装饰器时它所做的事情。

这主要与decorator模块如何尝试确定它正在装饰的函数的argspec有关。演示此行为的最简单方法是将使用@decorator.decorator制作的装饰器与手动制作的装饰器进行比较(为了实验目的,传递另一个对象已被替换为传递字符串):

import decorator

@decorator.decorator
def init_cart(f, *args, **kwargs):
    kwargs['shopping_cart'] = 'cart'
    print args, kwargs
    return f(*args, **kwargs)

@init_cart
def add_item(shopping_cart = None):
    print shopping_cart


def init_cart2(f):
    def wrapped(*args, **kwargs):
        kwargs['shopping_cart'] = 'cart'
        print args, kwargs
        return f(*args, **kwargs)
    return wrapped


@init_cart2
def add_item2(shopping_cart = None):
    print shopping_cart

因此,add_item2 将始终使用手动装饰器,而 add_item 将使用来自 decorator 模块的帮助程序。

我本来期望在两种情况下调用 add_item(shopping_cart = 'some value') 会导致 'cart' 被打印出来(在两种情况下,参数都是关键字参数,被装饰函数覆盖)。但实际上,以下结果发生了(两个装饰器都打印它们发送到包装函数的参数):

>>> add_item(shopping_cart = 'some_value')
('some_value',) {'shopping_cart': 'cart'}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 2, in add_item
  File "<stdin>", line 5, in init_cart
TypeError: add_item() got multiple values for keyword argument 'shopping_cart'

并且:

>>> add_item2(shopping_cart = 'some_value')
() {'shopping_cart': 'cart'}
cart

在第一个示例中,init_cart 没有作为关键字参数接收参数。
查看 decorator 模块的代码后,这一点变得更加清晰:
def decorator(caller, func=None):
    """
    decorator(caller) converts a caller function into a decorator;
    decorator(caller, func) decorates a function using a caller.
    """
    if func is None: # returns a decorator
        fun = FunctionMaker(caller)
        first_arg = inspect.getargspec(caller)[0][0]
        src = 'def %s(%s): return _call_(caller, %s)' % (
            caller.__name__, first_arg, first_arg)
        return fun.make(src, dict(caller=caller, _call_=decorator),
                        undecorated=caller)
    else: # returns a decorated function
        fun = FunctionMaker(func)
        src = """def %(name)s(%(signature)s):
    return _call_(_func_, %(signature)s)"""
        return fun.make(src, dict(_func_=func, _call_=caller), undecorated=func)

看起来包装的init_cart函数没有以与其包装器完全相同的方式被调用,这就是我的困惑所在。

回过头来看,这更有意义,可能是因为我使用了一个行为我不太理解的帮助程序。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接