快速了解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中urllib2模块的8个使用细节分享
Jan 01 Python
Linux下使用python自动修改本机网关代码分享
May 21 Python
Python实现的使用telnet登陆聊天室实例
Jun 17 Python
读写json中文ASCII乱码问题的解决方法
Nov 05 Python
在centos7中分布式部署pyspider
May 03 Python
Python中使用支持向量机(SVM)算法
Dec 26 Python
Python3 中把txt数据文件读入到矩阵中的方法
Apr 27 Python
浅谈Python爬虫基本套路
Mar 25 Python
python实现控制电脑鼠标和键盘,登录QQ的方法示例
Jul 06 Python
CentOS7下安装python3.6.8的教程详解
Jan 03 Python
Django+Celery实现动态配置定时任务的方法示例
May 26 Python
Lombok插件安装(IDEA)及配置jar包使用详解
Nov 04 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伪造referer突破网盘禁止外连的代码
2008/06/15 PHP
php class中self,parent,this的区别以及实例介绍
2013/04/24 PHP
ThinkPHP设置禁止百度等搜索引擎转码(简单实用)
2016/02/15 PHP
PHP7匿名类用法分析
2016/09/26 PHP
js 处理URL实用技巧
2010/11/23 Javascript
基于JQuery实现相同内容合并单元格的代码
2011/01/12 Javascript
javascript用户注册提示效果的简单实例
2013/08/17 Javascript
js鼠标悬浮出现遮罩层的方法
2015/01/28 Javascript
黑帽seo劫持程序,js劫持搜索引擎代码
2015/09/15 Javascript
不想让浏览器运行javascript脚本的方法
2015/11/20 Javascript
JQuery 在文档中查找指定name的元素并移除的实现方法
2016/05/19 Javascript
js 文字超出长度用省略号代替,鼠标悬停并以悬浮框显示实例
2016/12/06 Javascript
[原创]JS基于FileSaver.js插件实现文件保存功能示例
2016/12/08 Javascript
正则验证小数点后面只能有两位数的方法
2017/02/28 Javascript
基于JavaScript中字符串的match与replace方法(详解)
2017/12/04 Javascript
js常用正则表达式集锦
2019/05/17 Javascript
koa2 从入门到精通(小结)
2019/07/23 Javascript
你不知道的SpringBoot与Vue部署解决方案
2020/11/09 Javascript
python中类的一些方法分析
2014/09/25 Python
python编写爬虫小程序
2015/05/14 Python
Python中死锁的形成示例及死锁情况的防止
2016/06/14 Python
python中模块的__all__属性详解
2017/10/26 Python
python 去除txt文本中的空格、数字、特定字母等方法
2018/07/24 Python
Python生成器的使用方法和示例代码
2019/03/04 Python
Python如何调用外部系统命令
2019/08/07 Python
浅谈Pycharm的项目文件名是红色的原因及解决方式
2020/06/01 Python
Python图像读写方法对比
2020/11/16 Python
python遍历路径破解表单的示例
2020/11/21 Python
用HTML5实现手机摇一摇的功能的教程
2012/10/30 HTML / CSS
HTML5的新特性(1)
2016/03/03 HTML / CSS
英国知名的皮手套品牌:Dents
2016/11/13 全球购物
德国孕妇装和婴童服装网上商店:bellybutton
2018/04/12 全球购物
Skip Hop官网:好莱坞宝宝挚爱品牌
2018/06/17 全球购物
教师师德表现自我评价
2015/03/05 职场文书
Mysql8.0递归查询的简单用法示例
2021/08/04 MySQL
Python 游戏大作炫酷机甲闯关游戏爆肝数千行代码实现案例进阶
2021/10/16 Python