菊花链Python / Django自定义装饰器

时间:2014-03-26 03:39:52

标签: python django python-decorators

雏菊链Python / Django自定义装饰器的好风格吗?通过不同的论点而不是收到?

我的许多Django视图函数都以完全相同的代码开始:

@login_required
def myView(request, myObjectID):
    try:
        myObj = MyObject.objects.get(pk=myObjectID)
    except:
        return myErrorPage(request)       

    try:
        requester = Profile.objects.get(user=request.user)
    except:
        return myErrorPage(request)

    # Do Something interesting with requester and myObj here

仅供参考,这是urls.py文件中的相应条目:

url(r'^object/(?P<myObjectID>\d+)/?$', views.myView, ),

在许多不同的视图函数中重复相同的代码根本不是DRY。我想通过创建一个装饰器来改进它,这个装饰器可以为我做这个重复的工作,并使新的视图功能更清晰,看起来像这样:

@login_required
@my_decorator
def myView(request, requester, myObj):        
    # Do Something interesting with requester and myObj here

所以这是我的问题:

  1. 这是有效的吗?这是好风格吗?请注意,我将更改myView()函数的签名。这对我来说有点奇怪和危险。但我不确定为什么
  2. 如果我制作了多个这样的装饰器来执行一些常用功能,但是每个装饰器都使用与装饰器接收的参数不同的参数调用包装函数,那么如果我将它们连接在一起就可以吗?
  3. 如果上面的#1和#2没问题,那么向这个myView的用户指出他们应该传入的参数集的最佳方法是什么(因为只是查看函数定义中的参数已不再有效)

3 个答案:

答案 0 :(得分:5)

这是一个非常有趣的问题! Another one has already been answered in depth on the basic usage of decorators。但它没有提供很多关于修改参数的见解

Stackable decorators

你可以在另一个问题上找到一个堆叠装饰器的例子,下面的一段解释隐藏在非常非常长而详细的答案中:

  

是的,就是这样,就这么简单。 @decorator只是一个捷径:

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

这就是魔力。正如python documentation所述:装饰器是一个返回另一个函数的函数

这意味着你可以这样做:

from functools import wraps

def decorator1(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        do_something()
        f(*args, **kwargs)
    return wrapper


def decorator2(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        do_something_else()
        f(*args, **kwargs)
    return wrapper

@decorator1
@decorator2
def myfunc(n):
    print "."*n

#is equivalent to 

def myfunc(n):
    print "."*n
myfunc = decorator1(decorator2(myfunc))

装饰器不是装饰器

对于那些使用GoF 已经使用了一半字典来命名修复语言失败的模式的语言来学习OOP的开发人员来说,Python修饰者可能会感到困惑是事实上的设计模式店。

GoF的装饰器是他们正在装饰的组件(接口)的子类,因此与该组件的任何其他子类共享此接口。

Python装饰器是返回函数的函数(or classes)。

一直向下运行

python decorator是一个返回函数的函数,任何函数。

那里的大多数装饰器都是为了扩展装饰功能而不妨碍它的预期行为。它们是在GoF定义的Decorator模式之后形成的,它描述了一种在保持对象界面的同时扩展对象的方法。

但是GoF的Decorator是一种模式,而python的装饰是一种功能。

Python装饰器是函数,这些函数有望返回函数(当提供函数时)。

适配器

让我们采取另一种GoF模式:Adapter

  

适配器可帮助两个不兼容的接口协同工作。   这是适配器的真实世界定义。

     

[Object]适配器包含它包装的类的实例。   在这种情况下,适配器调用包装的实例   对象

以一个对象为例 - 比如一个调度程序,它会调用一个带有一些已定义参数的函数,并接受一个可以完成这项工作但提供另一组参数的函数。第二个函数的参数可以从第一个函数的参数中导出。

一个函数(在python中是一个第一类对象),它将获取第一个参数并派生它们来调用第二个并返回从其结果派生的值,这将是一个适配器。

为其传递的函数返回适配器的函数将是适配器工厂。

Python装饰器是返回函数的函数。包括适配器。

def my_adapter(f):
    def wrapper(*args, **kwargs):
        newargs, newkwargs = adapt(args, kwargs)
        return f(*newargs, **newkwargs)

@my_adapter # This is the contract provider
def myfunc(*args, **kwargs):
    return something()

哦,我看到你在那里做了什么......这是好风格吗?

我会说,到底是啊,还有另一种内置模式!但是你必须忘记GoF装饰器并且只记得python装饰器是返回函数的函数。因此,您正在处理的界面是包装函数之一,而不是装饰函数。

装饰一个函数后,装饰器定义了契约,要么告诉它保留装饰函数的界面,要么将其抽象掉。你不再调用那个装饰的函数了,尝试它也很棘手,你可以调用包装器。

答案 1 :(得分:4)

首先,这段代码:

try:
    myObj = MyObject.objects.get(pk=myObjectID)
except:
    return myErrorPage(request)

可以替换为:

from django.shortcuts import get_object_or_404
myObj = get_object_or_404(MyObject, pk=myObjectID)

同样适用于您拥有的第二个代码块。

这本身就使这更加优雅。

如果您想进一步实施自己的装饰器,最好的办法是将@login_required子类化。如果您通过不同的论点或者不想这样做,那么您确实可以自己做装饰,这不会错。

答案 2 :(得分:1)

1)是的,链接装饰器是有效的,因为其他答案已经指出。好的风格是主观的,但我个人认为这会让你的代码更难以为他人阅读。熟悉Django而不是你的应用程序的人需要在处理代码时保留额外的上下文。我认为坚持框架约定以使代码尽可能可维护非常重要。

2)答案是肯定的,技术上可以将不同的参数传递给包装函数,但考虑一个简单的代码示例,说明这将如何工作:

def decorator1(func):
    def wrapper1(a1):
        a2 = "hello from decorator 1"
        func(a1, a2)
    return wrapper1

def decorator2(func):
    def wrapper2(a1, a2):
        a3 = "hello from decorator 2"
        func(a1, a2, a3)
    return wrapper2

@decorator1
@decorator2
def my_func(a1, a2, a3):
    print a1, a2, a3

my_func("who's there?")

# Prints:
# who's there?
# hello from decorator 1
# hello from decorator2

在我看来,任何阅读此内容的人都需要成为心理体操运动员,以便在装饰者堆栈的每个级别保留方法签名的上下文。

3)我会使用基于类的视图并覆盖dispatch()方法来设置实例变量,如下所示:

class MyView(View):
    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        self.myObj = ...
        self.requester = ...
        return super(MyView, self).dispatch(*args, **kwargs)

dispatch方法调用您的get()/post()方法。来自django docs:

  

as_view入口点创建类的实例并调用其dispatch()方法。 dispatch查看请求以确定它是否是GET,POST等,并将请求中继到匹配方法(如果已定义)

然后,您可以在get()和/或post()视图方法中访问这些实例变量。这种方法的优点是您可以将其提取到基类并在任意数量的View子类中使用它。它在IDE中也更易于跟踪,因为这是标准继承。

get()请求的示例:

class MyView(View):
    def get(self, request, id):
        print 'requester is {}'.format(self.requester)