快速了解Python中的装饰器


Posted in Python onJanuary 11, 2018

需要理解的一些概念

要理解Python中的装饰器,我觉得还是应该从最基本的概念开始:

装饰器模式:所谓的装饰器模式,可以简单地理解为“在不改变原有内部实现的情况下,为函数或者类添加某种特性”。这样我们就可以将一些与业务无关、具有通用性的代码抽象出来,作为装饰器附加到需要这些代码的函数或者类之上。用面向切面编程的思想解释就是“装饰器应该是一个切面”。

函数是一等公民:意思就是函数可以被当成普通变量一样使用。在Python中,可以把函数赋值给变量,可以将函数作为其它函数的参数,也可以将函数作为其它函数的返回值。

闭包:我们都知道局部作用域可以引用全局作用域中的变量,相似的,当一个函数内部又定义了其它函数的时候,内部函数可以使用外部函数所在作用域的变量,这就是闭包。

从最简单的装饰器做起

理解完以上的概念之后,我们尝试一下利用这些特性实现一个简单的装饰器。

首先明确一下需求,我们有时候会需要在函数调用时打印一个相应的日志,虽然可以通过在所有需要打印日志的函数代码中嵌入打印日志的代码来实现,但这种方法不仅增加了许多重复代码,而且在业务代码中嵌入与业务无关的代码增加了整体的耦合度。因此,我们需要实现一个装饰器,这个装饰器在函数调用时可以打印一个日志记录函数调用行为。

如果我们有以下函数foo,代表具体的业务函数:

def foo():
  print('in function foo')

我们设想通过调用foo = deco(foo)实现给函数foo增加打印日志的功能,并且不影响它原有的业务。那么在这种设想下,装饰器deco应该也是一个函数,它接收另一个函数作为输入,并返回一个新的、经过装饰的函数。在Python中,我们可以这么写:

def deco(func): # 接收一个函数作为参数
  def new_func():
    print(f'[log] run function {func.__name__}') # 此处使用了Python3.6的格式化字符串
    func() # 闭包,在内部函数中使用了外部函数的变量
  return new_func # 将新函数作为返回值返回

执行一下试试效果:

>>> foo = deco(foo)
>>> foo()
[log] run function foo
in function foo

不错,至此我们已经实现了一个最简单的装饰器!在上面的代码中,装饰器deco接收任意的函数作为参数,再在内部构造另一个函数,利用闭包的特性,可以在新函数里调用存在于装饰器deco局部作用域中的函数func。

神奇的@

按照上面那么写,每次我们都得为需要装饰的函数赋一个新值,万一函数或者装饰器的数量增加了,手动写赋值和函数调用就会变得非常麻烦。那么在Python中,有没有更优雅的写法呢?答案是有的,你只需要一个@符号。

在Python中,当我们需要一个装饰器时:

def deco(func):
  def new_func():
    print(f'[log] run function {func.__name__}')
    func()
  return new_func

@deco
def foo():
  print('in function foo')

这个地方我们省略了函数的赋值,直接在函数foo定义的上一行加上@deco进行装饰。运行一下试试看:

>>> foo()
[log] run function foo
in function foo

是不是感觉很神奇?其实这里面没什么魔法,只不过是Python在处理函数定义的代码时,帮你在其中把foo=deco(foo)的逻辑加上了而已。

装饰器也想要参数

上面的代码实现了在业务函数调用之前打印日志的功能,那如果我们需要在业务代码执行完之后打印一条自定义的消息,怎么办呢?我们必须要让我们的装饰器可以接收自定义参数。

上面提到过,Python做的只是当写了@deco时,把调用deco(func)的结果赋值给它装饰的函数而已。顺着这个逻辑,当我们需要一个带参数的装饰器时,代码上应该是写为@deco('some message'),这时Python将调用deco(msg)(func)的结果赋值给foo。那么事情就变得简单了,我们只需要在上面代码的基础上嵌套一层函数:

def deco(msg):
  def inner_deco(func):
    def new_func():
      print(f'[log] run function {func.__name__}')
      func()
      print(f'[log] {msg}')
    return new_func
  return inner_deco

@deco('some message')
def foo():
  print('in function foo')

执行一下试试:

>>> foo()
[log] run function foo
in function foo
[log] some message

不支持带参数的被装饰函数的装饰器不是好装饰器

上面的代码还是有问题,因为我们只考虑了函数foo没有参数时的情况,万一函数foo带了参数,这个装饰器就会丢失参数信息,这不是一个合格的装饰器应该出现的情况。所以,我们借助Python中的*args和**kwargs使被装饰的函数可以支持传入任意参数:

def deco(msg):
  def inner_deco(func):
    def new_func(*args, **kwargs):
      print(f'[log] run function {func.__name__}')
      func(*args, **kwargs)
      print(f'[log] {msg}')
    return new_func
  return inner_deco

@deco('some message')
def foo(a, b=None):
  print('in function foo')
  print(f'a is {a} & b is {b}')

这样一来,无论函数foo的参数列表是怎么样的都不会有问题了:

>>> foo('hello')
[log] run function foo
in function foo
a is hello & b is None
[log] some message

不支持有返回值的被装饰函数的装饰器不是好装饰器

别忘了,到目前为止,我们写的函数foo都没有返回值,如果函数foo有返回值怎么办呢?我想你心里应该有答案了:

def deco(msg):
  def inner_deco(func):
    def new_func(*args, **kwargs):
      print(f'[log] run function {func.__name__}')
      rlt = func(*args, **kwargs)
      print(f'[log] {msg}')
      return rlt
    return new_func
  return inner_deco

@deco('some message')
def foo(a, b=None):
  print('in function foo')
  print(f'a is {a} & b is {b}')
  return 'ok'

由于装饰器在原函数执行完之后还有别的操作,所以应该把返回值暂存起来,等到装饰器的逻辑执行完毕,才返回最终结果。这就是我们的最终版装饰器了!

>>> rlt = foo('a')
[log] run function foo
in function foo
a is a & b is None
[log] some message
>>> print(rlt)
ok

有没有更骚的操作?

当然有啊!我标题都这么写了难不成会没有?

在Python中,你可以使用类来作为装饰器:

class Deco(object):
  def __call__(self, func):
    def new_func():
      print(f'[log] run function {func.__name__}')
      func()
    return new_func

@Deco()
def foo():
  print('in function foo')
>>> foo()
[log] run function foo
in function foo

这么做的好处就是可以利用类更好地管理参数和调用逻辑,比起之前三层函数嵌套的形式是不是清晰多了?

在Python中,你还可以使用装饰器来装饰一个类,比如这样:

def add_doc(doc):
  def deco(cls):
    cls.__doc__ = doc
    return cls
  return deco

@add_doc('this is the doc of Cls')
class Cls(object):
  pass

来看看效果:

>>> help(Cls)
Help on class Cls in module __main__:

class Cls(builtins.object)
 | this is the doc of Cls

上面的代码只是一个示例,展示装饰器怎么装饰一个类而已,不是说在实际情况下应该这么用。大部分的情况下,我们对于类的拓展应该是通过继承而不是装饰。

具体怎么巧妙地利用装饰器就要靠大家发挥自己的想象力了。

总结

以上就是本文关于快速了解Python中的装饰器的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

Python 相关文章推荐
详细解读Python的web.py框架下的application.py模块
May 02 Python
基于Python实现文件大小输出
Jan 11 Python
一个基于flask的web应用诞生 组织结构调整(7)
Apr 11 Python
python利用lxml读写xml格式的文件
Aug 10 Python
python 文件操作删除某行的实例
Sep 04 Python
PyQT实现菜单中的复制,全选和清空的功能的方法
Jun 17 Python
Flask项目中实现短信验证码和邮箱验证码功能
Dec 05 Python
Python图像处理库PIL的ImageFont模块使用介绍
Feb 26 Python
如何用python处理excel表格
Jun 09 Python
浅谈pytorch中torch.max和F.softmax函数的维度解释
Jun 28 Python
python3.4中清屏的处理方法
Jul 06 Python
10个python爬虫入门基础代码实例 + 1个简单的python爬虫完整实例
Dec 16 Python
简单了解python模块概念
Jan 11 #Python
100行Python代码实现自动抢火车票(附源码)
Jan 11 #Python
python实现外卖信息管理系统
Jan 11 #Python
Python实现学生成绩管理系统
Apr 05 #Python
名片管理系统python版
Jan 11 #Python
Python学生成绩管理系统简洁版
Apr 05 #Python
Python实现学校管理系统
Jan 11 #Python
You might like
《逃离塔科夫》——“萌新劝退,老手自嗨”的硬核FPS游戏
2020/04/03 其他游戏
比较全的PHP 会话(session 时间设定)使用入门代码
2008/06/05 PHP
PHP静态延迟绑定和普通静态效率的对比
2017/10/20 PHP
重定向实现代码
2006/11/20 Javascript
学习ExtJS(二) Button常用方法
2009/10/07 Javascript
javascript跟随滚动效果插件代码(javascript Follow Plugin)
2013/08/03 Javascript
js Array操作的最简短最容易理解方法
2013/12/09 Javascript
js中string转int把String类型转化成int类型
2014/08/13 Javascript
jQuery CSS()方法改变现有的CSS样式
2014/08/20 Javascript
深入浅析Node.js 事件循环
2015/12/20 Javascript
javascript类型系统——undefined和null全面了解
2016/07/13 Javascript
JS实现六边形3D拖拽翻转效果的方法
2016/09/11 Javascript
IOS中safari下的select下拉菜单文字过长不换行的解决方法
2016/09/26 Javascript
easyui导出excel无法弹出下载框的快速解决方法
2016/11/10 Javascript
微信开发 JS-SDK 6.0.2 经常遇到问题总结
2016/12/08 Javascript
JS实现动态给标签控件添加事件的方法示例
2017/05/13 Javascript
JS中使用cavas截图网页并解决跨域及模糊问题
2018/11/13 Javascript
详解vue组件中使用路由方法
2019/02/12 Javascript
Vue 处理表单input单行文本框的实例代码
2019/05/09 Javascript
JS将时间秒转换成天小时分钟秒的字符串
2019/07/10 Javascript
解决ele ui 表格表头太长问题的实现
2019/11/13 Javascript
JavaScript进阶(一)变量声明提升实例分析
2020/05/09 Javascript
完美解决vue 中多个echarts图表自适应的问题
2020/07/19 Javascript
python插入排序算法的实现代码
2013/11/21 Python
Python检测字符串中是否包含某字符集合中的字符
2015/05/21 Python
django模型层(model)进行建表、查询与删除的基础教程
2017/11/21 Python
python微信公众号之关键词自动回复
2018/06/15 Python
Python3获取拉勾网招聘信息的方法实例
2019/04/03 Python
python matplotlib包图像配色方案分享
2020/03/14 Python
大学生的四年学习自我评价
2013/12/13 职场文书
新党章心得体会
2014/09/04 职场文书
2014年党的群众路线活动个人整改措施
2014/10/28 职场文书
2015年学校减负工作总结
2015/05/19 职场文书
买卖合同纠纷代理词
2015/05/25 职场文书
60条职场经典语录,总有一条能触动你的心
2019/08/21 职场文书
python 算法题——快乐数的多种解法
2021/05/27 Python