Python面向函数编程——装饰器的实际应用

来源:互联网 发布:excel软件官方下载 编辑:程序博客网 时间:2024/05/21 00:18

转载来源:http://1.python123.sinaapp.com/?p=67  所有版权归原作者所有



对某个函数使用了装饰器之后,实际上是改变了函数的代码入口点。也就是变成了装饰器函数所返回的函数的代码入口点了。

 

机制

 

装饰器之所以能够工作,是因为Python是一个动态语言。函数是作为第一级对象存在的,就是说函数可以存储到变量中、作为参数传递给其他函数,最重要的是能够被函数动态地创建和返回。

而Python的装饰器就是一个函数,这个函数可以是内置的(比如@staticmethon、@classmethon),也可以是自定义的函数。这个函数接收另一个函数作为参数(也就是要被装饰的函数)。装饰器把被装饰的函数作为参数,然后充当装饰器的函数内可以有任意的操作,只要保证返回一个可执行的函数就可以了(这个函数既可以是被装饰的函数,也可以是一个全新的函数),可以想象其作用的强大了。

 错误的装饰器

 

 看看下面这个代码:

 

12345678910
def decorator1(fn):    print ('other operation 1')    return fn @decorator1def test1():    print ('hello test1') test1()test1()

输出

>>>other operation 1hello test1hello test1

这段代码中第5行是需要我们来关注的,Python中的装饰器就是通过@表示的。当执行到这一行时,其实执行的是decorator1(test1)这么个伪代码,于是乎:

1、  执行第二行代码,输出”other operation 1”;

2、  返回一个函数,这个函数恰好就是test1。

 

第5行代码执行完后,原来的函数test1的入口点就被修改了,不过恰好还是原来的入口点。

接下来执行第9、10行,这会执行装饰后的test1,只不过恰好装饰后的代码和原来的代码完全一样。

这种写法的装饰器(如果能叫做装饰器的话)其实并不是真正意义上的装饰器。因为它并没有给被装饰的函数添加任何的功能。装饰器(decorator1)函数体中的其它操作(print)只会被调用一次,也就是只有在第5行这个地方调用一次。有些网上文章说是【被装饰的函数首次调用时才会执行】,这是误导,你可以把第9、10行的代码去掉,不调用被装饰的函数,仍然会输出”other operation 1”字样。因此虽然两次调用了test(),但其实调用的都是test()自己,并没有任何附加的功能。要想保证其真的添加额外的功能,需要换种写法。

 

真正的装饰器

 

装饰器根据是否有参数分成两种:无参数和有参数。

这里所说的参数跟被装饰的函数没有任何关系,被修饰的函数有没有参数并不影响装饰器的分类。先来看无参数的装饰器。

 

无参数的装饰器

 

12345678910111213
def decorator2(fn):    print('only once')    def wrapper():        print('other operation 2')        return fn()    return wrapper     @decorator2def test2():    print ("hello test2") test2()test2()

第8行的执行方式:

1、  进入到decorator2函数入口;

2、  执行第2行,输出”only once”;也就是说这个语句只会执行一次;

3、  执行第3行,这是一个函数定义开始的地方,函数本身就是个对象;因此相当于创建了一个行数对象;

4、  执行到第6行,返回这个函数对象。

现在,test2()函数的入口点就被修改了,原来test2的入口点是第10行,现在变成了第4行了。

 

第12行的执行方式:

1、  进入test2的入口点,现在这个入口点是第4行了;

2、  执行第4行;

3、  执行第5行,进入test2函数的原来入口点——第10行;

4、  执行第10行。

 

输出

>>>onle onceother operation 2hello test2other operation 2hello test2

有参数的装饰器

 

修饰器也支持参数,不过这种修饰器在执行的时候,参数列表中并没有被修饰函数这个参数,因此必须首先返回一个decorator函数,由后者对被修饰的函数做处理。

 

12345678910111213141516
def decorator3(name):    def indecorator(fn):        print ('only once')        def wrapper():            print ('other operation 3'+name)            return fn()        return wrapper    return indecorator @decorator3('zhangxiaoming') def test3():    print ("hello test3") test3()test3()

第10行代码的执行是这样的:

1、  进入到函数decorator3的入口点(带着参数name,参数值是’zhangxiaoming’);

2、  执行第2行,这是一个函数定义,这才是真正的装饰器函数;

3、  执行第8行,返回这个函数装饰器;

4、  在执行这个函数装饰器,这回才把要装饰的函数test3作为参数传递进去;

5、  执行第4行,输出’only once’;

6、  执行第5行;

7、  执行第7行,返回装饰后的函数。

 

现在,函数test3的入口点就被修改了,修改后的入口点变成了第5行。

第15的执行是这样的:

1、  进入到test3的入口点,现在这个入口点变成了第5行;

2、  执行第5行;

3、  执行第6行,进入到test3原来的入口点,也就是第13行;

4、  执行第13行。

最终的输出是这样的:

>>>only onceother operation 3zhangxiaominghello test3other operation 3zhangxiaominghello test3

装饰器的作用

 

装饰器的应用场景是很丰富的,下面就用两个最常见的例子来解释。

身份验证

 

对于一个快速开发的网站程序,最开始的时候程序员只关注业务的功能,比如浏览、订单等功能,后来用户量上来了,希望加上身份认证的功能。但是对于不同的访问,验证要求又不一样,比如浏览产品功能不需要认证,只有要下订单时才需要认证。

怎么做呢,当然可以打开业务功能的代码,在里面加上认证逻辑,也就是加上一堆if…else…。但这样做势必要修改原来精心测试的代码,这些代码都需要重新测试。麻烦的还不只如此,以后身份认证机制可能会变,比如一开始用户名、密码记录在文本文件中,后来用了数据库,后来网站大了,又加上了SSO等等更复杂的验证方式。问题是,这些验证方法的逻辑只有安全工程师才懂,如果强迫业务程序员在他们的代码中加上这些逻辑会让蛋疼的。最好的方法就是装饰器:

123456789101112131415
#encoding=utf-8#web_mock.pydef GET():    print (u'通过Get方法访问') def POST(name):    print (u'通过POST方法访问')    print (u'你好 '+name.decode('utf-8')) print('*'*40)GET()print('*'*40)POST('huang')print('*'*40)POST('zhang xiao ming')

这是一个模拟的web页面的代码,这里没有任何身份验证,GET、POST方法完成的都是纯业务的逻辑。代码的输出是:

>>>****************************************通过Get方法访问****************************************通过POST方法访问你好 huang****************************************通过POST方法访问你好 zhang xiao ming

好了,我现在要加上身份认证了,当然我可以直接修改这个业务代码,像这样:

123456789101112131415161718
#encoding=utf-8 def GET():    print (u'通过Get方法访问') def POST(name):    if name=='zhang xiao ming':        print (u'通过POST方法访问')        print (u'你好 '+name.decode('utf-8'))    else :        print (u'POST方法需要身份认证') print('*'*40)GET()print('*'*40)POST('huang')print('*'*40)POST('zhang xiao ming')

这段代码的输出如下:

>>>****************************************通过Get方法访问****************************************POST方法需要身份认证****************************************通过POST方法访问你好 zhang xiao ming

问题是,我的程序中有大量的类似POST方法都需要加上身份认证,难道让我一个个的改过去吗?真蛋疼啊!

幸运的是,我学会了装饰器,于是我就可以这么做了,新建一个auth_deco.py,其中的代码是这样的:

1234567891011121314151617181920212223
#encoding=utf8#auth_deco.pydef webauth(conf):    def _authuser(fname,uname):        return uname=='zhang xiao ming'     def _noauthuser(fname):        print (unicode(fname)+u'需要提供用户名密码')     def auth(fn):        def wrapper(uname):            if _authuser(fn.__name__,uname):                fn(uname)            else :                _noauthuser(fn.__name__)        return wrapper     def noauth(fn):        def wrapper():            return fn()        return wrapper     return {'needauth':auth,'noauth':noauth}[conf]

现在,使用这个装饰器,web_mock.py中只需要加上三条语句就可以了:

1234567891011121314151617181920
#encoding=utf-8#web_mock.py from auth_deco import webauth @webauth('noauth')def GET():    print (u'通过Get方法访问') @webauth('needauth')def POST(name):    print (u'通过POST方法访问')    print (u'你好 '+name.decode('utf-8')) print('*'*40)GET()print('*'*40)POST('huang')print('*'*40)POST('zhang xiao ming')

输出是:

>>>****************************************通过Get方法访问****************************************POST需要提供用户名密码****************************************通过POST方法访问你好 zhang xiao ming

再来一个记录日志的例子
这个例子中,被装饰的方法本身可能也有参数。

123456789101112131415161718192021222324252627282930313233343536
#encoding=utf8 def loger(conf)def _log(f,*args,**kargs):        print (u"""        记录日志        调用:%s        参数:args(%r)             kargs(%r)        """ % (f,args,kargs))     def needlog(fn):        def wrapper(*args,**kargs):            _log(fn.__name__,*args,**kargs)            return fn(*args,**kargs)        return wrapper     def nolog(fn):        def wrapper(*args,**kargs):            print(u'不用做记录')            return fn(*args,**kargs)        return wrapper     return {'needlog':needlog,'nolog':nolog}[conf] @loger('needlog')def test1(name):    print ('hello '+name) @loger('nolog')def test2(name):    print ('hello2 '+name) test1('zhang xiao ming')test2('haha haha')

输出

>>>         记录日志        调用:test1        参数:args(('zhang xiao ming',))             kargs({})hello zhang xiao ming不用做记录hello2 haha haha

多个装饰器一起使用

这时候需要注意装饰器的顺序

12345678910111213141516
def head(fn):    def wrapper():        return ''+fn()+''    return wrapper def meta(fn):    def wrapper():        return ''+fn()+''    return wrapper @head@metadef test():    return 'test' print(test())

输出是:

>>><head><meta>test</meta></head>

原创粉丝点击