快速了解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编程中的for循环语句学习教程
Oct 14 Python
深入学习Python中的上下文管理器与else块
Aug 27 Python
windows下Virtualenvwrapper安装教程
Dec 13 Python
浅谈Python 列表字典赋值的陷阱
Jan 20 Python
Python列表常见操作详解(获取,增加,删除,修改,排序等)
Feb 18 Python
python多进程下实现日志记录按时间分割
Jul 22 Python
Python爬取腾讯视频评论的思路详解
Dec 19 Python
Scrapy框架介绍之Puppeteer渲染的使用
Jun 19 Python
浅析Python迭代器的高级用法
Jul 16 Python
浅析关于Keras的安装(pycharm)和初步理解
Oct 23 Python
教你如何使用Python下载B站视频的详细教程
Apr 29 Python
yolov5返回坐标的方法实例
Mar 17 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
PHP函数eval()介绍和使用示例
2014/08/20 PHP
php array_map使用自定义的函数处理数组中的每个值
2016/10/26 PHP
thinkPHP5框架自定义验证器实现方法分析
2018/06/11 PHP
javascript游戏开发之《三国志曹操传》零部件开发(四)用地图块拼成大地图
2013/01/23 Javascript
JS 实现导航栏悬停效果
2013/09/23 Javascript
firefox下jquery ajax返回object XMLDocument处理方法
2014/01/26 Javascript
用html+css+js实现的一个简单的图片切换特效
2014/05/28 Javascript
js实现九宫格图片半透明渐显特效的方法
2015/02/16 Javascript
基于JS实现导航条之调用网页助手小精灵的方法
2016/06/17 Javascript
ASP.NET jquery ajax传递参数的实例
2016/11/02 Javascript
JavaScript实现垂直向上无缝滚动特效代码
2016/11/23 Javascript
js 颜色选择插件
2017/01/23 Javascript
详解React Native顶|底部导航使用小技巧
2017/09/14 Javascript
用Node提供静态文件服务的方法
2018/07/06 Javascript
小程序获取周围IBeacon设备的方法
2018/10/31 Javascript
详解js 创建对象的几种方法
2019/03/08 Javascript
vue 项目build错误异常的解决方法
2019/04/22 Javascript
vue+Element实现搜索关键字高亮功能
2019/05/28 Javascript
Vue父组件监听子组件生命周期
2020/09/03 Javascript
详解JavaScript 的执行机制
2020/09/18 Javascript
uniapp微信小程序实现一个页面多个倒计时
2020/11/01 Javascript
神经网络理论基础及Python实现详解
2017/12/15 Python
Django中ajax发送post请求 报403错误CSRF验证失败解决方案
2019/08/13 Python
解决pyCharm中 module 调用失败的问题
2020/02/12 Python
python中导入 train_test_split提示错误的解决
2020/06/19 Python
详解python中的异常和文件读写
2021/01/03 Python
使用CSS禁止textarea调整大小功能的方法
2015/03/13 HTML / CSS
CSS3弹性盒模型flex box快速入门心得(必看篇)
2016/05/24 HTML / CSS
Interrail法国:乘火车探索欧洲,最受欢迎的欧洲铁路通票
2019/08/27 全球购物
乌克兰第一的珠宝网上商店:Gold.ua
2019/11/29 全球购物
Linux如何为某个操作添加别名
2013/03/01 面试题
JSF界面控制层技术
2013/06/17 面试题
大学生党课思想汇报
2013/12/29 职场文书
校本研修个人总结
2015/02/28 职场文书
2015年新农合工作总结
2015/03/30 职场文书
企业财务总监岗位职责
2015/04/03 职场文书