快速了解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语言技巧之三元运算符使用介绍
Mar 04 Python
Python打印斐波拉契数列实例
Jul 07 Python
详解Python3中yield生成器的用法
Aug 20 Python
Python脚本暴力破解栅栏密码
Oct 19 Python
Golang与python线程详解及简单实例
Apr 27 Python
Python实现的下载网页源码功能示例
Jun 13 Python
Python使用base64模块进行二进制数据编码详解
Jan 11 Python
python用户评论标签匹配的解决方法
May 31 Python
python3.X 抓取火车票信息【修正版】
Jun 19 Python
python opencv实现切变换 不裁减图片
Jul 26 Python
python 协程中的迭代器,生成器原理及应用实例详解
Oct 28 Python
python3操作注册表的方法(Url protocol)
Feb 05 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计算排列组合的方法
2013/11/13 PHP
PHP实例分享判断客户端是否使用代理服务器及其匿名级别
2014/06/04 PHP
利用PHP脚本在Linux下用md5函数加密字符串的方法
2015/06/29 PHP
ThinkPHP数据操作方法总结
2015/09/28 PHP
阿里云的WindowsServer2016上部署php+apache
2018/07/17 PHP
TP5(thinkPHP5框架)基于bootstrap实现的单图上传插件用法示例
2019/05/29 PHP
DHTML Slide Show script图片轮换
2008/03/03 Javascript
jQuery代码优化 选择符篇
2011/11/01 Javascript
JS获取图片实际宽高及根据图片大小进行自适应
2013/08/11 Javascript
使用nodejs开发cli项目实例
2015/06/03 NodeJs
使用AngularJS制作一个简单的RSS阅读器的教程
2015/06/18 Javascript
Boostrap入门准备之border box
2016/05/09 Javascript
详细谈谈AngularJS的子级作用域问题
2016/09/05 Javascript
JavaScript解决浮点数计算不准确问题的方法分析
2018/07/09 Javascript
vue的for循环使用方法
2019/02/12 Javascript
Vue的Eslint配置文件eslintrc.js说明与规则介绍
2020/02/03 Javascript
解决vue-photo-preview 异步图片放大失效的问题
2020/07/29 Javascript
解决windows下Sublime Text 2 运行 PyQt 不显示的方法分享
2014/06/18 Python
Python文件操作类操作实例详解
2014/07/11 Python
linux环境下python中MySQLdb模块的安装方法
2017/06/16 Python
Django Admin 实现外键过滤的方法
2017/09/29 Python
Django 2.0版本的新特性抢先看!
2018/01/05 Python
Python全栈之列表数据类型详解
2019/10/01 Python
CSS3解决移动页面上点击链接触发色块的问题
2016/06/03 HTML / CSS
微信html5页面调用第三方位置导航的示例
2018/03/14 HTML / CSS
前端实现弹幕效果的方法总结(包含css3和canvas的实现方式)
2018/07/12 HTML / CSS
男女时尚与复古风格在线购物:RoseGal(全球免费送货)
2017/07/19 全球购物
戴森香港官方网站:Dyson香港
2021/02/11 全球购物
毕业生的自我评价分享
2013/12/18 职场文书
家佳咖啡店创业计划书
2013/12/27 职场文书
职员竞岗演讲稿
2014/05/14 职场文书
医药销售自荐书
2014/05/29 职场文书
学生会竞选演讲稿学习部
2014/08/25 职场文书
话题作文之财富(600字)
2019/12/03 职场文书
Oracle11g R2 安装教程完整版
2021/06/04 Oracle
Python turtle实现贪吃蛇游戏
2021/06/18 Python