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写的创建文件夹自定义函数mkdir()
Aug 25 Python
利用Python的Django框架生成PDF文件的教程
Jul 22 Python
Python2.7基于淘宝接口获取IP地址所在地理位置的方法【测试可用】
Jun 07 Python
Python_LDA实现方法详解
Oct 25 Python
利用numpy实现一、二维数组的拼接简单代码示例
Dec 15 Python
Python基于高斯消元法计算线性方程组示例
Jan 17 Python
python 拼接文件路径的方法
Oct 23 Python
对Python3.x版本print函数左右对齐详解
Dec 22 Python
全面了解django的缓存机制及使用方法
Jul 22 Python
Python列表list常用内建函数实例小结
Oct 22 Python
基于Python中isfile函数和isdir函数使用详解
Nov 29 Python
Tensorflow 定义变量,函数,数值计算等名字的更新方式
Feb 10 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设计模式 FlyWeight (享元模式)
2011/06/26 PHP
php切割页面div内容的实现代码分享
2012/07/31 PHP
PHP循环函数使用介绍之PHP基础入门教程
2013/09/21 PHP
php数组索引的Key加引号和不加引号的区别
2014/08/19 PHP
php防止网站被攻击的应急代码
2015/10/21 PHP
js+CSS 图片等比缩小并垂直居中实现代码
2008/12/01 Javascript
基于Jquery的标签智能验证实现代码
2010/12/27 Javascript
Javascript调用C#代码
2011/01/17 Javascript
jquery 实现输入邮箱时自动补全下拉提示功能
2015/10/04 Javascript
canvas 画布在主流浏览器中的尺寸限制详细介绍
2016/12/15 Javascript
详解Vue2 无限级分类(添加,删除,修改)
2017/03/07 Javascript
利用JavaScript在网页实现八数码启发式A*算法动画效果
2017/04/16 Javascript
ionic+AngularJs实现获取验证码倒计时按钮
2017/04/22 Javascript
解决JS外部文件中文注释出现乱码问题
2017/07/09 Javascript
vue Element-ui input 远程搜索与修改建议显示模版的示例代码
2017/10/19 Javascript
一次Webpack配置文件的分离实战记录
2018/11/30 Javascript
vue计算属性get和set用法示例
2019/02/08 Javascript
微信接入之获取用户头像的方法步骤
2019/09/23 Javascript
原生js实现滑块区间组件
2021/01/20 Javascript
[03:27]最受玩家喜爱奖提名:PZH_Element 致玩家寄语
2016/12/20 DOTA
Python实现随机生成有效手机号码及身份证功能示例
2017/06/05 Python
利用Python实现Windows下的鼠标键盘模拟的实例代码
2017/07/13 Python
在Mac下使用python实现简单的目录树展示方法
2018/11/01 Python
用python打印菱形的实操方法和代码
2019/06/25 Python
python dict乱码如何解决
2020/06/07 Python
html5的自定义data-*属性与jquery的data()方法的使用
2014/07/02 HTML / CSS
详解HTML5中的元素与元素
2015/08/17 HTML / CSS
美国波道夫·古德曼百货官网:Bergdorf Goodman
2017/11/07 全球购物
公安四风对照检查材料思想汇报
2014/10/11 职场文书
党支部四风整改方案
2014/10/25 职场文书
党的群众路线教育实践活动总结
2014/10/30 职场文书
2014年宣传思想工作总结
2014/12/10 职场文书
2016年班主任新年寄语
2015/08/18 职场文书
python实现网络五子棋
2021/04/11 Python
LayUI+Shiro实现动态菜单并记住菜单收展的示例
2021/05/06 Javascript
铁拳制作人赞《铁拳7》老头环Mod:制作精良 但别弄了
2022/04/03 其他游戏