Python的装饰器使用详解


Posted in Python onJune 26, 2017

Python有大量强大又贴心的特性,如果要列个最受欢迎排行榜,那么装饰器绝对会在其中。

初识装饰器,会感觉到优雅且神奇,想亲手实现时却总有距离感,就像深闺的冰美人一般。这往往是因为理解装饰器时把其他的一些概念混杂在一起了。待我抚去层层面纱,你会看到纯粹的装饰器其实蛮简单直率的。

装饰器的原理

在解释器下跑个装饰器的例子,直观地感受一下。
# make_bold就是装饰器,实现方式这里略去

>>> @make_bold
... def get_content():
...  return 'hello world'
...
>>> get_content()
'<b>hello world</b>'

被 make_bold 装饰的 get_content ,调用后返回结果会自动被 b 标签包住。怎么做到的呢,简单4步就能明白了。

1. 函数是对象

我们定义个 get_content 函数。这时 get_content 也是个对象,它能做所有对象的操作。

def get_content():
  return 'hello world'

它有 id ,有 type ,有值。

>>> id(get_content)
140090200473112
>>> type(get_content)
<class 'function'>
>>> get_content
<function get_content at 0x7f694aa2be18>

跟其他对象一样可以被赋值给其它变量。

>>> func_name = get_content
>>> func_name()
'hello world'

它可以当参数传递,也可以当返回值

>>> def foo(bar):
...   print(bar())
...   return bar
...
>>> func = foo(get_content)
hello world
>>> func()
'hello world'

2. 自定义函数对象

我们可以用 class 来构造函数对象。有成员函数 __call__ 的就是函数对象了,函数对象被调用时正是调用的 __call__ 。

class FuncObj(object):
  def __init__(self, name):
    print('Initialize')
    self.name= name

  def __call__(self):
    print('Hi', self.name)

我们来调用看看。可以看到, 函数对象的使用分两步:构造和调用 (同学们注意了,这是考点)。

>>> fo = FuncObj('python')
Initialize
>>> fo()
Hi python

3. @ 是个语法糖

装饰器的 @ 没有做什么特别的事,不用它也可以实现一样的功能,只不过需要更多的代码。

@make_bold
def get_content():
  return 'hello world'

# 上面的代码等价于下面的

def get_content():
  return 'hello world'
get_content = make_bold(get_content)

make_bold 是个函数,要求入参是函数对象,返回值是函数对象。 @ 的语法糖其实是省去了上面最后一行代码,使可读性更好。用了装饰器后,每次调用 get_content ,真正调用的是 make_bold 返回的函数对象。

4. 用类实现装饰器

入参是函数对象,返回是函数对象,如果第2步里的类的构造函数改成入参是个函数对象,不就正好符合要求吗?我们来试试实现 make_bold 。

class make_bold(object):
  def __init__(self, func):
    print('Initialize')
    self.func = func

  def __call__(self):
    print('Call')
    return '<b>{}</b>'.format(self.func())

大功告成,看看能不能用。

>>> @make_bold
... def get_content():
...   return 'hello world'
...
Initialize
>>> get_content()
Call
'<b>hello world</b>'

成功实现装饰器!是不是很简单?

这里分析一下之前强调的 构造 和 调用 两个过程。我们去掉 @ 语法糖好理解一些。
# 构造,使用装饰器时构造函数对象,调用了__init__

>>> get_content = make_bold(get_content)
Initialize

# 调用,实际上直接调用的是make_bold构造出来的函数对象
>>> get_content()
Call
'<b>hello world</b>'

到这里就彻底清楚了,完结撒花,可以关掉网页了~~~(如果只是想知道装饰器原理的话)

函数版装饰器

阅读源码时,经常见到用嵌套函数实现的装饰器,怎么理解?同样仅需4步。

1. def 的函数对象初始化

用 class 实现的函数对象很容易看到什么时候 构造 的,那 def 定义的函数对象什么时候 构造 的呢?
# 这里的全局变量删去了无关的内容

>>> globals()
{}
>>> def func():
...   pass
...
>>> globals()
{'func': <function func at 0x10f5baf28>}

不像一些编译型语言,程序在启动时函数已经构造那好了。上面的例子可以看到,执行到 def 会才构造出一个函数对象,并赋值给变量 make_bold 。

这段代码和下面的代码效果是很像的。

class NoName(object):
  def __call__(self):
    pass

func = NoName()

2. 嵌套函数

Python的函数可以嵌套定义。

def outer():
  print('Before def:', locals())
  def inner():
    pass
  print('After def:', locals())
  return inner

inner 是在 outer 内定义的,所以算 outer 的局部变量。执行到 def inner 时函数对象才创建,因此每次调用 outer 都会创建一个新的 inner 。下面可以看出,每次返回的 inner 是不同的。

>>> outer()
Before def: {}
After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa0048>}
<function outer.<locals>.inner at 0x7f0b18fa0048>
>>> outer()
Before def: {}
After def: {'inner': <function outer.<locals>.inner at 0x7f0b18fa00d0>}
<function outer.<locals>.inner at 0x7f0b18fa00d0>

3. 闭包

嵌套函数有什么特别之处?因为有闭包。

def outer():
  msg = 'hello world'
  def inner():
    print(msg)
  return inner

下面的试验表明, inner 可以访问到 outer 的局部变量 msg 。

>>> func = outer()
>>> func()
hello world

闭包有2个特点
1. inner 能访问 outer 及其祖先函数的命名空间内的变量(局部变量,函数参数)。
2. 调用 outer 已经返回了,但是它的命名空间被返回的 inner 对象引用,所以还不会被回收。

这部分想深入可以去了解Python的LEGB规则。

4. 用函数实现装饰器

装饰器要求入参是函数对象,返回值是函数对象,嵌套函数完全能胜任。

def make_bold(func):
  print('Initialize')
  def wrapper():
    print('Call')
    return '<b>{}</b>'.format(func())
  return wrapper

用法跟类实现的装饰器一样。可以去掉 @ 语法糖分析下 构造 和 调用 的时机。

>>> @make_bold
... def get_content():
...   return 'hello world'
...
Initialize
>>> get_content()
Call
'<b>hello world</b>'

因为返回的 wrapper 还在引用着,所以存在于 make_bold 命名空间的 func 不会消失。 make_bold 可以装饰多个函数, wrapper 不会调用混淆,因为每次调用 make_bold ,都会有创建新的命名空间和新的 wrapper 。

到此函数实现装饰器也理清楚了,完结撒花,可以关掉网页了~~~(后面是使用装饰的常见问题)

常见问题

1. 怎么实现带参数的装饰器?

带参数的装饰器,有时会异常的好用。我们看个例子。

>>> @make_header(2)
... def get_content():
...   return 'hello world'
...
>>> get_content()
'<h2>hello world</h2>'

怎么做到的呢?其实这跟装饰器语法没什么关系。去掉 @ 语法糖会变得很容易理解。

@make_header(2)
def get_content():
  return 'hello world'

# 等价于

def get_content():
  return 'hello world'
unnamed_decorator = make_header(2)
get_content = unnamed_decorator(get_content)

上面代码中的 unnamed_decorator 才是真正的装饰器, make_header 是个普通的函数,它的返回值是装饰器。

来看一下实现的代码。

def make_header(level):
  print('Create decorator')

  # 这部分跟通常的装饰器一样,只是wrapper通过闭包访问了变量level
  def decorator(func):
    print('Initialize')
    def wrapper():
      print('Call')
      return '<h{0}>{1}</h{0}>'.format(level, func())
    return wrapper

  # make_header返回装饰器
  return decorator

看了实现代码,装饰器的 构造 和 调用 的时序已经很清楚了。

>>> @make_header(2)
... def get_content():
...   return 'hello world'
...
Create decorator
Initialize
>>> get_content()
Call
'<h2>hello world</h2>'

2. 如何装饰有参数的函数?

为了有条理地理解装饰器,之前例子里的被装饰函数有意设计成无参的。我们来看个例子。

@make_bold
def get_login_tip(name):
  return 'Welcome back, {}'.format(name)

最直接的想法是把 get_login_tip 的参数透传下去。

class make_bold(object):
  def __init__(self, func):
    self.func = func

  def __call__(self, name):
    return '<b>{}</b>'.format(self.func(name))

如果被装饰的函数参数是明确固定的,这么写是没有问题的。但是 make_bold 明显不是这种场景。它既需要装饰没有参数的 get_content ,又需要装饰有参数的 get_login_tip 。这时候就需要可变参数了。

class make_bold(object):
  def __init__(self, func):
    self.func = func
  def __call__(self, *args, **kwargs):
    return '<b>{}</b>'.format(self.func(*args, **kwargs))

当装饰器不关心被装饰函数的参数,或是被装饰函数的参数多种多样的时候,可变参数非常合适。可变参数不属于装饰器的语法内容,这里就不深入探讨了。

3. 一个函数能否被多个装饰器装饰?

下面这么写合法吗?

@make_italic
@make_bold
def get_content():
  return 'hello world'

合法。上面的的代码和下面等价,留意一下装饰的顺序。

def get_content():
  return 'hello world'
get_content = make_bold(get_content) # 先装饰离函数定义近的
get_content = make_italic(get_content)

4. functools.wraps 有什么用?

Python的装饰器倍感贴心的地方是对调用方透明。调用方完全不知道也不需要知道调用的函数被装饰了。这样我们就能在调用方的代码完全不改动的前提下,给函数patch功能。

为了对调用方透明,装饰器返回的对象要伪装成被装饰的函数。伪装得越像,对调用方来说差异越小。有时光伪装函数名和参数是不够的,因为Python的函数对象有一些元信息调用方可能读取了。为了连这些元信息也伪装上, functools.wraps 出场了。它能用于把被调用函数的 __module__ , __name__ , __qualname__ , __doc__ , __annotations__ 赋值给装饰器返回的函数对象。

import functools

def make_bold(func):
  @functools.wraps(func)
  def wrapper(*args, **kwargs):
    return '<b>{}</b>'.format(func(*args, **kwargs))
  return wrapper

对比一下效果。

>>> @make_bold
... def get_content():
...   '''Return page content'''
...   return 'hello world'

# 不用functools.wraps的结果
>>> get_content.__name__
'wrapper'
>>> get_content.__doc__
>>>

# 用functools.wraps的结果
>>> get_content.__name__
'get_content'
>>> get_content.__doc__
'Return page content'

实现装饰器时往往不知道调用方会怎么用,所以养成好习惯加上 functools.wraps 吧。

这次是真·完结了,撒花吧~~~

Python 相关文章推荐
下载糗事百科的内容_python版
Dec 07 Python
Python中使用MELIAE分析程序内存占用实例
Feb 18 Python
解决pycharm每次新建项目都要重新安装一些第三方库的问题
Jan 17 Python
python切片的步进、添加、连接简单操作示例
Jul 11 Python
python 函数的缺省参数使用注意事项分析
Sep 17 Python
Django Docker容器化部署之Django-Docker本地部署
Oct 09 Python
python连接打印机实现打印文档、图片、pdf文件等功能
Feb 07 Python
解决Python中报错TypeError: must be str, not bytes问题
Apr 07 Python
python3爬虫中异步协程的用法
Jul 10 Python
python爬取天气数据的实例详解
Nov 20 Python
Python基础之赋值,浅拷贝,深拷贝的区别
Apr 30 Python
使用Djongo模块在Django中使用MongoDB数据库
Jun 20 Python
Python学习思维导图(必看篇)
Jun 26 #Python
python flask 多对多表查询功能
Jun 25 #Python
Python的语言类型(详解)
Jun 24 #Python
Python队列的定义与使用方法示例
Jun 24 #Python
Python实现字符串逆序输出功能示例
Jun 24 #Python
Python正则表达式分组概念与用法详解
Jun 24 #Python
Python正则表达式常用函数总结
Jun 24 #Python
You might like
基于mysql的论坛(7)
2006/10/09 PHP
php第一次无法获取cookie问题处理
2014/12/15 PHP
PHP实现股票趋势图和柱形图
2015/02/07 PHP
js下通过getList函数实现分页效果的代码
2010/09/17 Javascript
JQuery获取与设置HTML元素的内容或文本的实现代码
2014/06/20 Javascript
fckeditor粘贴Word时弹出窗口取消的方法
2014/10/30 Javascript
js实现二代身份证号码验证详解
2014/11/20 Javascript
jQuery中animate用法实例分析
2015/03/09 Javascript
JS实现获取当前URL和来源URL的方法
2016/08/24 Javascript
vue数据双向绑定原理解析(get &amp; set)
2017/03/08 Javascript
JS排序之选择排序详解
2017/04/08 Javascript
JS中LocalStorage与SessionStorage五种循序渐进的使用方法
2017/07/12 Javascript
对vuejs的v-for遍历、v-bind动态改变值、v-if进行判断的实例讲解
2018/08/27 Javascript
vue添加axios,并且指定baseurl的方法
2018/09/19 Javascript
基于vue的验证码组件的示例代码
2019/01/22 Javascript
[36:20]完美世界DOTA2联赛PWL S3 access vs Rebirth 第一场 12.17
2020/12/18 DOTA
python类定义的讲解
2013/11/01 Python
跟老齐学Python之深入变量和引用对象
2014/09/24 Python
在Heroku云平台上部署Python的Django框架的教程
2015/04/20 Python
python清除指定目录内所有文件中script的方法
2015/06/30 Python
python多线程socket编程之多客户端接入
2017/09/12 Python
利用Hyperic调用Python实现进程守护
2018/01/02 Python
使用Django和Python创建Json response的方法
2018/03/26 Python
Python实现定时执行任务的三种方式简单示例
2019/03/30 Python
Django 对象关系映射(ORM)源码详解
2019/08/06 Python
python给视频添加背景音乐并改变音量的具体方法
2020/07/19 Python
python中绕过反爬虫的方法总结
2020/11/25 Python
HTTP状态码详解
2021/03/18 杂记
理肤泉美国官网:La Roche-Posay
2018/01/17 全球购物
英国第一家领先的在线处方眼镜零售商:Glasses Direct
2018/02/23 全球购物
英国家电购物网站:Sonic Direct
2019/03/26 全球购物
POS解决方案:MUNBYN(热敏打印机、条形码扫描仪)
2020/06/09 全球购物
解释i节点在文件系统中的作用
2013/11/26 面试题
高考自主招生自荐信
2013/10/20 职场文书
省文明单位申报材料
2014/05/08 职场文书
篮球赛口号
2014/06/18 职场文书