使用类来写 Python 的 decorator
平时看到的 Python 的 decorator 都是使用函数来写的,比如说我之前在写的 login_required
1def login_required(func):
2 @wraps(func)
3 def check(*args, **kwargs):
4 # 在class based views 里面,args 有两个元素,一个是self, 第二个才是request,
5 # 在function based views 里面,args 只有request 一个参数
6 request = args[0]
7 if request.user.is_authenticated():
8 return func(*args, **kwargs)
9 if request.is_ajax():
10 return error_response(u"请先登录")
11 else:
12 return HttpResponseRedirect("/login/")
13 return check
14
15@login_required
16def view(request):
17 pass
这样的话,在调用 view 函数的时候就相当于 login_required(view)
了。
其实我们使用类也能完成这样的任务,调用函数就相当于 DecoratorClass(func)()
了,后面的括号和调用函数的意思差不多,是在__call__
方法中定义的,所以我们就可以这样写
1class login_required(object):
2 def __init__(self, func):
3 self.func = func
4
5 def __call__(self, *args, **kwargs):
6 print args, kwargs
7 return self.func(*args, **kwargs)
8
9@login_required
10def index(*args, **kwargs):
11 print "index page"
12
13index(1, 2, 3, a=4, b=5)
输出就是
1__call__ (1, 2, 3) {'a': 4, 'b': 5}
2index page
看起来一切正常。
至于为什么使用类呢。今天下午在写代码的时候,发现有三个 decorator,结构基本一致,就是判断条件有差别,分别的login_required
、admin_required
和super_admin_required
。这不能忍,我就想到了使用类来写,因为可以继承啊,很快就写好了。
1class BasePermissionDecorator(object):
2 def __init__(self, func):
3 self.func = func
4
5 def __get__(self, obj, obj_type):
6 return functools.partial(self.__call__, obj)
7
8 def __call__(self, *args, **kwargs):
9 if len(args) == 2:
10 self.request = args[1]
11 else:
12 self.request = args[0]
13
14 if self.check_permission():
15 return self.func(*args, **kwargs)
16 else:
17 if self.request.is_ajax():
18 return error_response(u"请先登录")
19 else:
20 return HttpResponseRedirect("/login/")
21
22 def check_permission(self):
23 raise NotImplementedError()
24
25
26class login_required(BasePermissionDecorator):
27 def check_permission(self):
28 return self.request.user.is_authenticated()
就只贴 login_required
这一个吧,剩下的基本一样。
然后在运行之前的测试用例的时候出错了,这个 decorator 用在类方法上的时候就会报错,self 参数没了,而普通 def 的函数没问题,demo 如下。
1class IndexView(object):
2 @login_required
3 def post(self, request):
4 print self, request
5
6IndexView()post(1, 2, 3, a=4, b=5)
输出如下
1__call__ (1, 2, 3) {'a': 4, 'b': 5}
21 (2, 3) {'a': 4, 'b': 5}
发现 self 参数没了,匹配到了参数1。
而正常的调用应该是
1<__main__.IndexView object at 0x1013dd890> (1, 2, 3) {'a': 4, 'b': 5}
后来查了一下,说是在 decorator 中需要定义一个__get__
方法,
1class BasePermissionDecorator(object):
2 def __init__(self, func):
3 self.func = func
4
5 def __get__(self, obj, obj_type):
6 return functools.partial(self.__call__, obj)
7
8 def __call__(self, *args, **kwargs):
9 if len(args) == 2:
10 self.request = args[1]
11 else:
12 self.request = args[0]
13
14 if self.check_permission():
15 return self.func(*args, **kwargs)
16 else:
17 if self.request.is_ajax():
18 return error_response(u"请先登录")
19 else:
20 return HttpResponseRedirect("/login/")
原因如下。
1class IndexView(object):
2 @login_required
3 def post(self, request):
4 print self, request
相当于
1class IndexView(object):
2 def post(self, request):
3 print self, request
4 post = login_required(post)
所以
1view = IndexView()
2view.post
就相当于view.post.__get__(post, view, <type of view>)
就相当于login_required(post).__get__(view, <type of view>)
,也就是调用的class login_required
里面的__get__
方法,因为我们使用了partial
函数,相当于返回了一个指定了一个参数为 view 的函数,然后再调用的时候再增加一个其他参数。
至于为什么会调用__get__
函数,可以参考http://intermediatepythonista.com/classes-and-objects-ii-descriptors,这是 Python 的 descriptor。