Python 装饰器


Python 中有一个非常有趣的特性 - 装饰器,它允许我们动态地更改行为或扩展函数的功能。

装饰器比较难懂,但是一旦理解,便能用它做很多功能强大的事情,比如:日志打印、性能检测、事务处理、缓存、权限校验等。

在深入理解装饰器之前,最好先了解一下函数的一些高级用法(参考:Python 函数是第一类对象Python 闭包)。

1

空装饰器

所谓空装饰器,是指一个什么都不做的装饰器,这可以说是 Python 中最简单的装饰器。之所以介绍它,仅仅是为了解释语法而已!

一起来看看,如何定义一个空装饰器:

>>> def null_decorator(func):
...     return func
...
>>>

这里,null_decorator 是一个高阶函数,它接收一个函数作为输入,并将其直接返回(不做任何修改)。

现在,我们再定义一个函数:

>>> def greet():
...     print('Hello')
...
>>>

然后用 null_decorator 来装饰它:

>>> greet = null_decorator(greet)
>>> greet()
Hello

虽然语在法上没任何问题,但是这种写法不太优雅,所以 Python 支持了 @ 语法糖:

>>> @null_decorator
... def greet():
...     print('Hello')
...
>>>
>>> greet()
Hello

这等同于上面的写法,只不过更加简便罢了。

2

装饰无参函数

在对语法有所了解之后,是时候编写一个有实际操作的装饰器了。

假设,要定义一个打印日志的装饰器,可以这样写:

>>> def log(func):
...     def wrapper():
...         print('call {}()'.format(func.__name__))
...         return func()
...     return wrapper
...
>>>

不同于上面的空装饰器,这个装饰器并非简单地返回输入函数,而是动态地定义了一个新函数(闭包)- wrapper(),并使用它来包装输入函数,以便在调用时修改它的行为。

如果用它装饰原始的 greet() 函数,会发生什么?

>>> @log
... def greet():
...     print('Hello')
...
>>>
>>> greet()
call greet()
Hello

可以看到,在调用 greet() 函数时,不仅会运行函数本身,还会在运行之前打印一行日志。

3

装饰器链

此外,Python 也支持装饰器链(即:将多个装饰器应用于同一函数),这会累积它们的效果。

例如,再定义一个测试性能的装饰器:

>>> import time
>>>
>>> def performance(func):
...     def wrapper():
...         start_time = time.time()  # 开始时间
...         print('start time: {}'.format(start_time))
...         r = func()
...         end_time = time.time()    # 结束时间
...         print('end time: {}'.format(end_time))
...         print('take {} seconds'.format(end_time - start_time))  # 消耗的时间
...         return r
...     return wrapper
...
>>>

然后,将 log 和 performance 装饰器同时应用于 greet() 函数:

>>> @log
... @performance
... def greet():
...     print('Hello')
...
>>>

如果运行函数,你期望看到什么结果?是先执行 @log,还是 @performance 呢?

>>> greet()
call wrapper()
start time: 1559795538.9981225
Hello
end time: 1559795539.0026038
take 0.004481315612792969 seconds
>>>

很明显,这清楚地说明了装饰器的应用顺序:从下到上(即:先执行 @performance,然后执行 @log)。

如果分解上面的例子,那么函数的调用链如下所示:

greet = log(performance(greet))

先将 greet 应用于 performance,然后将结果应用于 log,从而得到包装后的函数。

4

装饰带参数的函数

上面的装饰器很简单,但仅适用于没有任何参数的函数。倘若我们的函数包含参数,该怎么办?

例如,为 greet() 函数添加一个参数,用于自定义问候语句:

>>> @log
... def greet(name):
...     print('Hello, {}.'.format(name))
...
>>>
>>> greet('Waleon')
...
TypeError: wrapper() takes 0 positional arguments but 1 was given

咦,分明和上面的写法一样,这里为何出错了呢?这是因为 wrapper() 不接受参数,而我们在传递时却给它指定了一个!

要解决这个问题,则需要对 log 略作修改:

>>> def log(func):
...     def wrapper(name):     # 加上参数
...         print('call {}()'.format(func.__name__))
...         return func(name)  # 调用时,也应该匹配
...     return wrapper
...
>>>

现在,再来尝试一下:

>>> @log
... def greet(name):
...     print('Hello, {}.'.format(name))
...
>>>
>>> greet('Waleon')
call greet()
Hello, Waleon.

虽然程序正常运行,但这并不是完美方案,因为这个装饰器不适用于任意数量参数的函数。

其实,实现这样一个通用装饰器非常简单,这个魔法交由 * args 和 ** kwargs 来完成就好了:

>>> def log(func):
...     def wrapper(*args, **kwargs):
...         print('call {}()'.format(func.__name__))
...         return func(*args, **kwargs)
...     return wrapper
...
>>>

不妨尝试一下,使用不同数量参数的函数:

>>> @log
... def greet():      # 无参
...     print('Hello')
...
>>> greet()  
call greet()
Hello
>>>
>>> @log
... def greet(name):  # 一个参数
...     print('Hello, {}.'.format(name))
...
>>> greet('Waleon')  
call greet()
Hello, Waleon.

通常,还可以用这种方式来追踪函数的参数以及返回值。

5

复制元数据

在使用装饰器时,我们是将一个函数替换为另一个函数。但这个过程有一个缺点,就是它隐藏了原始函数附带的一些元数据。

例如,原始函数的名称、docstring 和参数列表:

>>> @log
... def greet():
...     '''greet to someone'''
...     print('Hello')
...
>>>

如果尝试访问该函数的元数据,将得到的是装饰器中闭包的元数据:

>>> greet.__name__
'wrapper'
>>> print(greet.__doc__)
None

这会使调试变得困难,值得庆幸的是,Python 提供了一个快速解决方案 - functools.wraps。

可以在装饰器中使用它,以将原始函数的元数据复制到 wrapper() 中:

>>> import functools
>>>
>>> def log(func):
...     @functools.wraps(func)  # 重点
...     def wrapper(*args, **kwargs):
...         print('call {}()'.format(func.__name__))
...         return func(*args, **kwargs)
...     return wrapper
...
>>>

验证一下,和期望结果一样:

>>> @log
... def greet():
...     '''greet to someone'''
...     print('Hello')
...
>>>
>>> greet.__name__
'greet'
>>> greet.__doc__
'greet to someone'

建议:应尽量在装饰器中使用 functools.wraps,这是一种很好的编程习惯。不需要花费太多时间,还能避免调试带来的麻烦,何乐而不为!

·END·
 

高效程序员

谈天 · 说地 · 侃代码 · 开车

长按识别二维码,解锁更多精彩内容

零基础掌握 Python 入门到实战

11-08
【为什么学PythonPython 是当今非常热门的语言之一,2020年的 TIOBE 编程语言排行榜中 ,Python名列第一,并且其流行度依然处在上升势头。 在2015年的时候,在网上还经常看到学Python还是学R的讨论,那时候老齐就选择了Python,并且开始着手出版《跟老齐学Python》。时至今日,已经无需争论。Python给我们带来的,不仅仅是项目上的收益,我们更可以从它“开放、简洁”哲学观念中得到技术发展路线的启示。 借此机会,老齐联合CSDN推出了本课程,希望能影响更多的人走进Python,踏入编程的大门。 【课程设计】 本课程共包含三大模块: 一、基础知识篇 内置对象和基本的运算、语句,是Python语言的基础。本课程在讲解这部分知识的时候,不是简单地将各种知识做简单的堆砌,而是在兼顾内容的全面性的同时,更重视向学习者讲授掌握有关知识的方法,比如引导学习者如何排查错误、如何查看和理解文档等。   二、面向对象篇 “面向对象(OOP)”是目前企业开发主流的开发方式,本课程从一开始就渗透这种思想,并且在“函数”和“类”的学习中强化面向对象开发方式的学习——这是本课程与一般课程的重要区别,一般的课程只在“类”这里才提到“对象”,会导致学习者茫然失措,并生畏惧,乃至于放弃学习。本课程则是从开始以“润物细无声”的方式,渗透对象概念,等学习到本部分的时候,OOP对学习者而言有一种“水到渠成”的感觉。   三、工具实战篇 在项目实战中,除了前述的知识之外,还会用到很多其他工具,至于那些工具如何安装?怎么自己做工具?有那些典型工具?都是这部分的内容。具体来说,就是要在这部分介绍Python标准库的应用以及第三方包的安装,还有如何开发和发布自己的工具包。此外,很多学习Python的同学,未来要么从事数据科学、要么从事Web开发,不论哪个方向,都离不开对数据库的操作,本部分还会从实战的角度,介绍如何用Python语言操作常用数据库。
©️2020 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值