Python装饰器详解

在上一篇文章中我们提到了闭包,也就是将函数作为返回值返回。闭包搞懂了之后,接下来的内容就很简单了。

在定义了许多函数之后,我们希望扩展这些函数的功能,譬如在函数调用前后自动打印日志,但如果是一些通用的功能,修改每一个函数又会显得比较麻烦。最好的方法就是定义一个装饰器,给每个函数增加功能。这种在代码运行期间动态增加函数功能的方式,成为装饰器(Decorator)

一、初始函数

>>> from datetime import datetime
>>> def now():
        print(datetime.now())
>>> now()
2016-05-31 17:04:40.946448


二、增加功能

>>> def now():
        print('run %s().' % now.__name__)
        print(datetime.now())
        print('run %s() finished.' % now.__name__)
>>> now()
run now().
2016-05-31 17:13:15.216500
run now() finished.


这是最普通的增加函数功能的方式了,但如果将它变成通用功能呢?使用装饰器!

三、装饰器

>>> def decorator(fun):
        def wrapper(*args,**kw):
            print('run %s().' % fun.__name__)
            outcome = fun(*args,**kw)
            print('run %s() finished.' % fun.__name__)
            return outcome
        return wrapper
>>> now = decorator(now)
>>> now()
run now().
2016-05-31 17:15:08.001145
run now() finished.


这是装饰器最直观的表示了,定义一个装饰器函数,然后将自己的函数传入,输出的新函数即添加了新功能。

本质上来看,装饰器就是一个高阶函数。由于其接收参数为(*args,**kw),所以能够接受任何形式的调用。在wrapper函数内,调用传入的fun()函数之外,就可以定义新的功能。

但是now = decorator(now)这样的方式似乎很麻烦,所以Python提供了装饰器的语法糖@,使得能够简便地使用装饰器。

四、语法糖
语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

>>> def decorator(fun):
        def wrapper(*args,**kw):
            print('run %s().' % fun.__name__)
            outcome = fun(*args,**kw)
            print('run %s() finished.' % fun.__name__)
            return outcome
        return wrapper
>>> @decorator
    def now():
        print(datetime.now())
>>> now()
run now().
2016-05-31 17:10:32.217998
run now() finished.


只需要把@decorator放在函数定义的顶端,再调用函数即为添加功能的新函数。

五、__name__问题

>>> now()
run now().
2016-05-31 17:10:32.217998
run now() finished.
>>> now.__name__
'wrapper'


调用完成之后,新的问题又出现了:函数的__name__属性因为使用装饰器改变了,有些依赖函数签名的代码执行就会出错。

python内置的functools.wraps能够将函数名称替换回来。一个完整的decorator写法如下:

>>> import functools
>>> def decorator(fun):
        @functools.wraps(fun)
        def wrapper(*args,**kw):
            print('run %s().' % fun.__name__)
            outcome = fun(*args,**kw)
            print('run %s() finished.' % fun.__name__)
            return outcome
        return wrapper


你看,实际上这是对wrapper函数的一个装饰器。定义完成之后,就不会除问题了。

>>> @decorator
    def now():
        print(datetime.now())
>>> now()
run now().
2016-05-31 17:32:59.270645
run now() finished.
>>> now.__name__
'now'


六、带参数的Decorator
好了,现在我们已经知道如何实现一个完整的Decorator了,但是,针对不同的函数,我们希望同一个Decorator根据传入的参数,实现不同的功能,这应该怎么实现?

这种情况就需要更复杂的嵌套,首先,需要传入一个参数,然后返回一个装饰器函数,执行上面类似的工作,也就是说,在外面再嵌套一层就可以了。

>>> def decorator(arg):
        def wrapper(func):
            @functools.wraps(func)
            def function(*args,**kw):
                print('%s %s().' % (arg, func.__name__))
                outcome = func(*args,**kw)
                print('%s %s() finished.' % (arg, func.__name__))
                return outcome
            return function
        return wrapper


decorator接收参数,返回wrapper函数,而这个wrapper函数是一个装饰器。内部的运行如下:

>>> decorator('execute')
<function decorator.<locals>.wrapper at 0x0346B6F0>
>>> now = decorator('execute')(now)
>>> now()
execute now().
2016-05-31 19:15:46.935403
execute now() finished.
>>> now.__name__
'now'


decorator('execute')返回的是wrapper函数,然后再传入now函数,最终返回带有新功能&新参数的函数。

最终,使用语法糖的简便写法如下:

>>> @decorator('execute')
    def now():
        print(datetime.now())
>>> now()
execute now().
2016-05-31 19:11:19.024181
execute now() finished.

>>> @decorator('run')
    def now():
        print(datetime.now())
>>> now()
run now().
2016-05-31 19:11:45.361102
run now() finished.


七、终极装饰器
那么,问题来了,我们能不能创建一个装饰器,能够传入参数,也可以不传入参数呢?

这需要对装饰器的运行过程有充分的了解,观察上面的两类装饰器,主要的区别在于:

没有参数的装饰器,最外层接收的是func函数,最终返回的函数是wrapper(*args,**kw),直接接收func函数参数
带参数的装饰器,最外层接收的是参数,而最终返回的函数是wrapper(func),接收func函数
那么,我们就可以通过判定最外层的函数接收的是func函数还是参数,来决定是有参数或者是无参数的装饰器,最终决定如何返回。尝试结果如下:

>>> def decorator(arg):
        if callable(arg):
            @functools.wraps(arg)
            def wrapper(*args,**kw):
                print('run %s().' % arg.__name__)
                outcome = arg(*args,**kw)
                print('run %s() finished.' % arg.__name__)
                return outcome
            return wrapper
        else:
            def wrapper(func):
                @functools.wraps(func)
                def function(*args,**kw):
                    print('%s %s().' % (arg,func.__name__))
                    outcome = func(*args,**kw)
                    print('%s %s() finished.' % (arg,func.__name__))
                    return outcome
                return function
            return wrapper


通过最外层传入的参数是否callable()判断是函数还是参数,进而采取不一样的措施。

>>> @decorator
    def now():
        print(datetime.now())
>>> now()
run now().
2016-05-31 19:51:33.730241
run now() finished.

>>> @decorator('execute')
    def now():
        print(datetime.now())
>>> now()
execute now().
2016-05-31 19:53:09.032116
execute now() finished.


当然,还有另外一种思路,主体按照带参数的装饰器三层嵌套来写,但是最后返回的时候,需要进行判断:如果最初传入的是参数,那么就返回函数;如果最初传入的就是函数,那么就将第二层函数执行后返回。只不过会在内部替换参数的时候有一些局限。

>>> def decorator(arg):
    text = arg if isinstance(arg,str) else ''
    def wrapper(func):
        @functools.wraps(func)
        def function(*args,**kw):
            print(text)
            print('run %s().' % func.__name__)
            outcome = func(*args,**kw)
            print('run %s() finished.' % func.__name__)
            return outcome
        return function
    return wrapper if isinstance(arg,str) else wrapper(arg)


总结
到这里,装饰器的各部分内容都已经很清晰了。了解装饰器首先一定要将闭包是将函数和自由变量共同返回这一概念透彻清晰,其次,装饰器的内部实现原理一定要很了解,这样就没问题了。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值