使用Django的时候,我发现一个很神奇的装饰器:@login_required, 这是控制一个view的权限的,比如一个视图必须登录才可以访问,可以这样用:
| 1 2 3 4 | @login_required def my_view(request):     ...     return render(...) | 
同时,如果要达到这样一种效果:如果用户没有登录,那么就把用户重定向到登录界面,可以这样用:
| 1 2 3 4 | @login_required(login_url='/accounts/login/') def my_view(request):     ...     return render(...) | 
所以这个装饰器可以带括号写,又可以不带括号写。很神奇有没有。正常的接收参数的装饰器,就算没参数也应该写成@login_required()的
好奇去查了一下,在stackoverflow找到一种实现,挺有意思的。先晒出答案:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def doublewrap(f):     '''     a decorator decorator, allowing the decorator to be used as:     @decorator(with, arguments, and=kwargs)     or     @decorator     '''     @wraps(f)     def new_dec(*args, **kwargs):         if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):             # actual decorated function             return f(args[0])         else:             # decorator arguments             return lambda realf: f(realf, *args, **kwargs)     return new_dec | 
使用起来很简单,只要给装饰器用@doublewrap装饰一下,这个装饰器就支持写括号和不写括号两种写法了。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | def test_doublewrap():     from util import doublewrap     from functools import wraps         @doublewrap     def mult(f, factor=2):         '''multiply a function's return value'''         @wraps(f)         def wrap(*args, **kwargs):             return factor*f(*args,**kwargs)         return wrap     # try normal     @mult     def f(x, y):         return x + y     # try args     @mult(3)     def f2(x, y):         return x*y     # try kwargs     @mult(factor=5)     def f3(x, y):         return x - y     assert f(2,3) == 10     assert f2(2,5) == 30     assert f3(8,1) == 5*7 | 
原理也不难,只有短短不到10行代码。
装饰器我们都知道,是用来处理一个函数,返回一个新的函数的(如果你不理解装饰器,可以看一下这个经典的解释)。
| 1 | new_func = decorator(func) | 
我们使用的,就是被装饰器装饰的新函数了。装饰器只是一个语法糖,其实它也是一个函数,给它传入一个函数作为参数,就返回一个新的函数。那么既然装饰器也是一个函数,我们就可以用装饰器装饰这个函数。也就是,“装饰器的装饰器”。
装饰器第一个参数肯定是原函数,如果装饰器可以接收参数的话,要么第一个参数是原函数,后面跟别的参数;要么就只有原函数一个参数。所以,我们这个“装饰器的装饰器”做的事情就是,判断装饰器接收的参数,如果只有一个并且第一个参数是可调用的(callable),那么这就是一个无参数的装饰器(不需要加括号)。如果还有别的参数,就返回一个生成装饰器的函数(decorator_maker)。
装饰器是一个函数。装饰器被装饰过之后,这个装饰器运行之前就会先运行装饰器的装饰器的代码,也就是我们的doublewrapp。然后返回值可能是一个装饰器,也可能是一个装饰器的maker(有参数的装饰器),然后装饰器再执行,装饰原函数。
这里有点绕,因为本来装饰器里面一般就会有三四层函数了,(maker, decorator, wrapper, realfunc),再加上一个装饰器的装饰器,会有点理解困难。如果理解不了,最好不要对着网上的博文(包括本文)企图格物致知了,多去看看代码,多写一写。
2017年12月31日更新:《Python3 Cookbook》中提供了另一种实现方法,代码如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | from functools import wraps, partial import logging def logged(func=None, *, level=logging.DEBUG, name=None, message=None):     if func is None:         return partial(logged, level=level, name=name, message=message)     logname = name if name else func.__module__     log = logging.getLogger(logname)     logmsg = message if message else func.__name__     @wraps(func)     def wrapper(*args, **kwargs):         log.log(level, logmsg)         return func(*args, **kwargs)     return wrapper # Example use @logged def add(x, y):     return x + y @logged(level=logging.CRITICAL, name='example') def spam():     print('Spam!') | 
这种原理也比较好理解,看例子1,我们知道这等价于logged(add),第一个参数是函数,所以直接返回。看例子2,等价于logged(level=logging.CRITICAL, name='example')(spam),logged的第一个参数func是None(如果是装饰器语法,第一个参数会默认传入函数,而这里直接是函数调用,并非装饰器。所以会返回一个partial函数,这个partial函数是真正的装饰器,然后再走后面装饰器的路子。其实,这种方法也是利用了“判断传入装饰器的参数”。
Pingback: Python 复用装饰器代码 | 卡瓦邦噶!
> 所以这个装饰器可以带括号写,又可以不带括号写。很神奇有没有。正常的接收参数的装饰器,就算没参数也应该写成@login_required的
这个地方是不是笔误了,应该是@login_required()
谢谢,已经改正!