快速了解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进程的信号通讯
Apr 09 Python
小小聊天室Python代码实现
Aug 17 Python
详解使用 pyenv 管理多个版本 python 环境
Oct 19 Python
Python文本特征抽取与向量化算法学习
Dec 22 Python
Python计算时间间隔(精确到微妙)的代码实例
Feb 26 Python
Python字典遍历操作实例小结
Mar 05 Python
Python使用pandas和xlsxwriter读写xlsx文件的方法示例
Apr 09 Python
Django框架实现分页显示内容的方法详解
May 10 Python
了解不常见但是实用的Python技巧
May 23 Python
Python3实现配置文件差异对比脚本
Nov 18 Python
Python3自带工具2to3.py 转换 Python2.x 代码到Python3的操作
Mar 03 Python
http通过StreamingHttpResponse完成连续的数据传输长链接方式
Feb 12 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序列号生成函数和字符串替换函数代码
2012/06/07 PHP
php二维数组用键名分组相加实例函数
2013/11/06 PHP
php socket实现的聊天室代码分享
2014/08/16 PHP
PHP操作文件的一些基本函数使用示例
2014/11/18 PHP
PHP对称加密函数实现数据的加密解密
2016/10/27 PHP
javascript setTimeout()传递函数参数(包括传递对象参数)
2010/04/07 Javascript
jquery动画3.创建一个带遮罩效果的图片走廊
2012/08/24 Javascript
Jquery选择子控件"大于号"和" "区别介绍及使用示例
2013/06/25 Javascript
js函数内变量的作用域分析
2015/01/12 Javascript
移动端日期插件Mobiscroll.js使用详解
2016/12/19 Javascript
前端面试知识点锦集(JavaScript篇)
2016/12/28 Javascript
JavaScript实现鼠标点击导航栏变色特效
2017/02/08 Javascript
JavaScript数据类型的存储方法详解
2017/08/25 Javascript
JavaScript数组排序reverse()和sort()方法详解
2017/12/24 Javascript
微信小程序中使用wxss加载图片并实现动画效果
2018/08/13 Javascript
zepto.js 实时监听输入框的方法
2018/12/04 Javascript
javascript实现商品图片放大镜
2019/11/28 Javascript
vue Cli 环境删除与重装教程 - 版本文档
2020/09/11 Javascript
[01:17]辉夜杯战队访谈宣传片—EHOME
2015/12/25 DOTA
Python中的with...as用法介绍
2015/05/28 Python
解决Python pandas df 写入excel 出现的问题
2018/07/04 Python
Python实现决策树并且使用Graphviz可视化的例子
2019/08/09 Python
Numpy之将矩阵拉成向量的实例
2019/11/30 Python
容易被忽略的Python内置类型
2020/09/03 Python
app内嵌H5 webview 本地缓存问题的解决
2020/10/19 HTML / CSS
使用layui框架实现点击左侧导航切换右侧内容且右侧选项卡跟随变化的效果
2020/11/10 HTML / CSS
美国战术品牌:5.11 Tactical
2019/05/01 全球购物
大学生物业管理求职信
2013/10/24 职场文书
中专生自我鉴定书范文
2013/12/28 职场文书
幼儿园教师辞职信
2014/01/18 职场文书
大学四年个人自我小结
2014/03/05 职场文书
毕业生应聘求职信
2014/07/10 职场文书
敬业奉献模范事迹材料(2016精选版)
2016/02/26 职场文书
接触艺术对孩子学习思维有益
2019/08/06 职场文书
导游词之山东八仙过海景区
2019/11/11 职场文书
springboot中一些比较常用的注解总结
2021/06/11 Java/Android