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

Posted

技术标签:

【中文标题】菊花链 Python/Django 自定义装饰器【英文标题】:daisy-chaining Python/Django custom decorators 【发布时间】:2014-05-04 06:24:26 【问题描述】:

是一种好的风格吗?并传递与收到的不同的参数?

我的许多 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

所以这是我的问题:

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

【问题讨论】:

堆叠装饰器在技术上没有什么问题,但如果你在很多视图中都需要它,我建议扩展一个基于类的视图来封装 login_required 装饰器以保持你的代码干了。 但是如果装饰器不是简单的传递,而是将不同的参数传递给包装函数而不是接收到的参数,情况会变得更糟吗? 好问题。有更多经验的人愿意发表评论吗? 【参考方案1】:

这是一个非常有趣的问题! Another one has already been answered in depth on the basic usage of 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))

装饰器不是装饰器

Python 装饰器可能会让学习 OOP 的开发人员感到困惑,因为 GoF 已经使用了一半的字典来命名修复语言故障的模式 是事实上的设计模式商店。

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

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

功能一路向下

python 装饰器是一个返回函数的函数,任何函数。

大多数装饰器旨在扩展被装饰的功能,而不会妨碍它的预期行为。它们是在 GoF 对装饰器模式的定义之后形成的,该模式描述了一种在扩展对象的同时保持其接口的方法。

但是GoF的装饰器是一种模式,而python的装饰器是一种特性。

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

适配器

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

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

[An 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 装饰器是返回函数的函数。因此,您正在处理的接口是包装函数之一,而不是装饰函数。

一旦你装饰了一个函数,装饰器就会定义契约,要么告诉它保留被装饰函数的接口,要么将其抽象出来。你不再调用那个装饰函数,尝试它甚至很棘手,你调用了包装器。

【讨论】:

“GoF”是什么意思? @mak GoF 的意思是 Gang of Four,指的是 Design Patterns: Elements of Reusable Object-Oriented Software 一书的四位作者 好的,谢谢,我知道四人帮,但不知道缩写词。【参考方案2】:

首先,这段代码:

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。如果您传递不同的参数或不想这样做,那么您确实可以制作自己的装饰器,这不会错。

【讨论】:

【参考方案3】:

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() 视图方法中访问这些实例变量。这种方法的优点是您可以将其提取到基类中并在任意数量的 View 子类中使用它。它在 IDE 中也更容易追踪,因为这是标准继承。

get() 请求的示例:

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

【讨论】:

MyView 类中的 get 和 post 方法是什么样的?什么时候调用 dispatch() 方法?除了使用method_decorator,你能用上面的decorator1和decorator2来说明你的答案吗? 嗨@spinlok,如果您能回答我在上面评论中发布的问题,我会将赏金转给您,因为您的答案将优于发布的其他答案。 @SaqibAli 我添加了有关何时调用调度方法以及get() 方法的外观的更多细节。 method_decorator 只是一个适配器,用于在方法上使用常用的函数装饰器。我的方法根本不使用堆叠装饰器,所以我没有使用decorator2 好的。这种方法似乎有效。但一个问题是,我有很多这样的观点。而在每一个视图中,我都需要对self.requester和self.myObj进行操作。因此,如果我将您的调度代码复制到每个这样的视图中,那么我不得不重复自己很多次,对吗?那不是干的。所以我想我必须在 DRYness 和可维护性之间做出权衡?

以上是关于菊花链 Python/Django 自定义装饰器的主要内容,如果未能解决你的问题,请参考以下文章

自定义表单装饰器中的 Zend 框架类

有啥方法可以获取具有装饰器、Python、Django 的视图名称列表

python Django装饰器(定时器,调试)

python Django自带的类装饰器

尝试将装饰器添加到 models.Manager 时出现 Python Django 错误

NestJS 自定义装饰器返回未定义