链式使用Python/Django自定义装饰器

6 投票
3 回答
948 浏览
提问于 2025-04-17 23:55

把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, ),

在很多不同的视图函数中重复相同的代码,这样可不符合“不要重复自己”的原则。我想通过创建一个装饰器来改善这个问题,让这个重复的工作由装饰器来完成,这样新的视图函数就会更简洁,看起来像这样:

@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 个回答

5

首先,这段代码:

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这个装饰器继承。如果你需要传递不同的参数,或者不想这样做,那你完全可以自己写一个装饰器,这样也是没问题的。

6

这是个非常有趣的问题!另外一个问题已经详细回答了装饰器的基本用法。不过它对修改参数的内容没有提供太多见解。

可叠加的装饰器

在那个问题中,你可以找到一个叠加装饰器的例子,里面有一段非常、非常长且详细的解释:

没错,就是这么简单。@decorator 其实是一个快捷方式:

another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)

这就是其中的奥秘。正如Python 文档所说:装饰器是 返回另一个函数的函数

这意味着你可以这样做:

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))

装饰器并不是 装饰器

对于那些用其他语言学习面向对象编程的开发者来说,Python 的装饰器可能会让人感到困惑,因为在那些语言中,GoF 已经用了一半的字典来命名修复语言缺陷的模式 是公认的设计模式来源。

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

而 Python 的装饰器是返回函数的函数(或者类)。

函数的层层嵌套

Python 的装饰器是返回函数的函数,可以是任何函数。

大多数装饰器的设计目的是在不干扰被装饰函数的预期行为的情况下扩展它们。它们的形状遵循 GoF 对装饰器模式的定义,这种模式描述了一种在保持对象接口的同时扩展对象的方法。

但是,GoF 的装饰器是一个模式,而 Python 的装饰器是一个特性。

Python 的装饰器是函数,这些函数在接收到一个函数时,应该返回另一个函数。

适配器

我们再来看一个 GoF 的模式:适配器

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

[一个对象]适配器包含了它所包装的类的一个实例。 在这种情况下,适配器会调用被包装对象的实例。

举个例子,假设有一个调度器对象,它会调用一个需要某些特定参数的函数,而另一个函数可以完成这个工作,但需要不同的参数。第二个函数的参数可以从第一个函数的参数中派生出来。

一个函数(在 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 的装饰器是返回函数的函数。因此,你所处理的接口是包装函数的接口,而不是被装饰的函数的接口。

一旦你装饰了一个函数,装饰器就定义了合同,要么说明它保持被装饰函数的接口,要么将其抽象掉。你不再直接调用那个被装饰的函数,甚至尝试调用它都很棘手,你调用的是包装函数。

2

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的文档:

as_view 入口点会创建你的类的一个实例,并调用它的 dispatch() 方法。dispatch 会查看请求,以确定它是 GET、POST 等,并将请求转发到匹配的方法(如果有定义的话)。

然后你可以在你的 get() 和/或 post() 视图方法中访问这些实例变量。这种方法的好处是你可以把它提取到一个基类中,然后在任意数量的视图子类中使用。而且在IDE中,这样的继承关系也更容易追踪。

以下是一个 get() 请求的示例:

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

撰写回答