Python 装饰器深入理解


Posted in Python onMarch 16, 2017

讲 Python 装饰器前,我想先举个例子,虽有点污,但跟装饰器这个话题很贴切。

每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,让它变得更厚更长,这样一来,它不仅有遮羞功能,还能提供保暖,不过有个问题,这个内裤被我们改造成了长裤后,虽然还有遮羞功能,但本质上它不再是一条真正的内裤了。于是聪明的人们发明长裤,在不影响内裤的前提下,直接把长裤套在了内裤外面,这样内裤还是内裤,有了长裤后宝宝再也不冷了。装饰器就像我们这里说的长裤,在不影响内裤作用的前提下,给我们的身子提供了保暖的功效。

谈装饰器前,还要先要明白一件事,Python 中的函数和 Java、C++不太一样,Python 中的函数可以像普通变量一样当做参数传递给另外一个函数,例如:

def foo():
  print("foo")

def bar(func):
  func()

bar(foo)

正式回到我们的主题。装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

先来看一个简单例子,虽然实际代码可能比这复杂很多:

def foo():
  print('i am foo')

现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码:

def foo():
  print('i am foo')
  logging.info("foo is running")

如果函数 bar()、bar2() 也有类似的需求,怎么做?再写一个 logging 在 bar 函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个新的函数:专门处理日志 ,日志处理完之后再执行真正的业务代码

def use_logging(func):
  logging.warn("%s is running" % func.__name__)
  func()

def foo():
  print('i am foo')

use_logging(foo)

这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑 foo 函数,而是换成了 use_logging 函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个 foo 函数作为参数传递给 use_logging 函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。

简单装饰器

def use_logging(func):

def wrapper():
    logging.warn("%s is running" % func.__name__)
return func()  # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
return wrapper

def foo():
  print('i am foo')

foo = use_logging(foo) # 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于 foo = wrapper
foo()          # 执行foo()就相当于执行 wrapper()

use_logging 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 use_logging 装饰了一样,use_logging 返回的也是一个函数,这个函数的名字叫 wrapper。在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程。

@ 语法糖

如果你接触 Python 有一段时间了的话,想必你对 @ 符号一定不陌生了,没错 @ 符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。

def use_logging(func):

def wrapper():
    logging.warn("%s is running" % func.__name__)
return func()
return wrapper

@use_logging
def foo():
  print("i am foo")

foo()

如上所示,有了 @ ,我们就可以省去foo = use_logging(foo)这一句了,直接调用 foo() 即可得到想要的结果。你们看到了没有,foo() 函数不需要做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样,如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。

*args、**kwargs

可能有人问,如果我的业务逻辑函数 foo 需要参数怎么办?比如:

def foo(name):
  print("i am %s" % name)

我们可以在定义 wrapper 函数的时候指定参数:

def wrapper(name):
    logging.warn("%s is running" % func.__name__)
return func(name)
return wrapper

这样 foo 函数定义的参数就可以定义在 wrapper 函数中。这时,又有人要问了,如果 foo 函数接收两个参数呢?三个参数呢?更有甚者,我可能传很多个。当装饰器不知道 foo 到底有多少个参数时,我们可以用 *args 来代替:

def wrapper(*args):
    logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper

如此一来,甭管 foo 定义了多少个参数,我都可以完整地传递到 func 中去。这样就不影响 foo 的业务逻辑了。这时还有读者会问,如果 foo 函数还定义了一些关键字参数呢?比如:

def foo(name, age=None, height=None):
  print("I am %s, age %s, height %s" % (name, age, height))

这时,你就可以把 wrapper 函数指定关键字函数:

def wrapper(*args, **kwargs):
# args是一个数组,kwargs一个字典
    logging.warn("%s is running" % func.__name__)
return func(*args, **kwargs)
return wrapper

带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 foo 。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
        logging.warn("%s is running" % func.__name__)
elif level == "info":
        logging.info("%s is running" % func.__name__)
return func(*args)
return wrapper

return decorator

@use_logging(level="warn")
def foo(name='foo'):
  print("i am %s" % name)

foo()

上面的 use_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我 们使用@use_logging(level=”warn”)调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。

@use_logging(level=”warn”)等价于@decorator

类装饰器

没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

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

def __call__(self):
print ('class decorator runing')
    self._func()
print ('class decorator ending')

@Foo
def bar():
print ('bar')

bar()

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、__name__、参数列表,先看例子:

# 装饰器
def logged(func):
def with_logging(*args, **kwargs):
print func.__name__   # 输出 'with_logging'
print func.__doc__    # 输出 None
return func(*args, **kwargs)
return with_logging

# 函数
@logged
def f(x):
"""does some math"""
return x + x * x

logged(f)

不难发现,函数 f 被with_logging取代了,当然它的docstring,__name__就是变成了with_logging函数的信息了。好在我们有functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。

from functools import wraps
def logged(func):
  @wraps(func)
def with_logging(*args, **kwargs):
print func.__name__   # 输出 'f'
print func.__doc__    # 输出 'does some math'
return func(*args, **kwargs)
return with_logging

@logged
def f(x):
"""does some math"""
return x + x * x

装饰器顺序

一个函数还可以同时定义多个装饰器,比如:

@a
@b
@c
def f ():
  pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于

f = a(b(c(f)))

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

Python 相关文章推荐
Python使用reportlab将目录下所有的文本文件打印成pdf的方法
May 20 Python
Django中传递参数到URLconf的视图函数中的方法
Jul 18 Python
Python  pip安装lxml出错的问题解决办法
Feb 10 Python
Python实现求一个集合所有子集的示例
May 04 Python
使用python脚本实现查询火车票工具
Jul 19 Python
Django中使用第三方登录的示例代码
Aug 20 Python
python实现浪漫的烟花秀
Jan 30 Python
解决django服务器重启端口被占用的问题
Jul 26 Python
Pycharm远程调试原理及具体配置详解
Aug 08 Python
python中return不返回值的问题解析
Jul 22 Python
详解Scrapy Redis入门实战
Nov 18 Python
python使用yaml 管理selenium元素的示例
Dec 01 Python
WINDOWS 同时安装 python2 python3 后 pip 错误的解决方法
Mar 16 #Python
Django卸载之后重新安装的方法
Mar 15 #Python
Python json 错误xx is not JSON serializable解决办法
Mar 15 #Python
200行自定义python异步非阻塞Web框架
Mar 15 #Python
Python实现简易端口扫描器代码实例
Mar 15 #Python
Python使用sftp实现上传和下载功能(实例代码)
Mar 14 #Python
Python实现Youku视频批量下载功能
Mar 14 #Python
You might like
分享一个php 的异常处理程序
2014/06/22 PHP
初识laravel5
2015/03/02 PHP
centos7上编译安装php7以php-fpm方式连接apache
2018/11/08 PHP
php使用gearman进行任务分发操作实例详解
2020/02/26 PHP
php开发最强大的IDE编辑的phpstorm 2020.2配置Xdebug调试的详细教程
2020/08/17 PHP
JQuery获取当前屏幕的高度宽度的实现代码
2011/07/12 Javascript
jQuery 瀑布流 浮动布局(一)(延迟AJAX加载图片)
2012/05/23 Javascript
点击进行复制的JS代码实例
2013/08/23 Javascript
动态添加删除表格行的js实现代码
2014/02/28 Javascript
javascript简单实现滑动菜单效果的方法
2015/07/27 Javascript
JS加载器如何动态加载外部js文件
2016/05/26 Javascript
jQuery实现两个select控件的互移操作
2016/12/22 Javascript
jQuery实现拖拽可编辑模块功能代码
2017/01/12 Javascript
JS异步文件上传(兼容IE8+)
2017/04/02 Javascript
AngularJS日期格式化常见操作实例分析
2018/05/17 Javascript
分享5个好用的javascript文件上传插件
2018/09/16 Javascript
web.py中调用文件夹内模板的方法
2014/08/26 Python
Python文本特征抽取与向量化算法学习
2017/12/22 Python
详解Python3 中hasattr()、getattr()、setattr()、delattr()函数及示例代码数
2018/04/18 Python
Python3.5实现的三级菜单功能示例
2019/03/25 Python
django 框架实现的用户注册、登录、退出功能示例
2019/11/28 Python
python实现横向拼接图片
2020/03/23 Python
python 将视频 通过视频帧转换成时间实例
2020/04/23 Python
Python计算矩阵的和积的实例详解
2020/09/10 Python
Python调用飞书发送消息的示例
2020/11/10 Python
CSS3实现酷炫的3D旋转透视效果
2019/11/21 HTML / CSS
旧时光糖果:Old Time Candy
2018/02/05 全球购物
全球最大的生存食品、水和装备专用在线市场:BePrepared.com
2020/01/02 全球购物
员工自我鉴定范文
2013/10/06 职场文书
畜牧兽医本科生个人的自我评价
2013/10/11 职场文书
绩效管理实施方案
2014/03/19 职场文书
无犯罪记录证明范本
2014/09/15 职场文书
2014年学生会干事工作总结
2014/11/07 职场文书
慰问信范文
2015/02/14 职场文书
实习生个人总结范文
2015/02/28 职场文书
结婚司仪主持词
2015/06/29 职场文书