Python函数装饰器原理与用法详解


Posted in Python onAugust 16, 2019

本文实例讲述了Python函数装饰器原理与用法。分享给大家供大家参考,具体如下:

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

严格来说,装饰器只是语法糖,装饰器是可调用的对象,可以像常规的可调用对象那样调用,特殊的地方是装饰器的参数是一个函数

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

import time
#遵守开放封闭原则
def foo():
  start = time.time()
  # print(start) # 1504698634.0291758从1970年1月1号到现在的秒数,那年Unix诞生
  time.sleep(3)
  end = time.time()
  print('spend %s'%(end - start))
foo()

bar()、bar2()也有类似的需求,怎么做?再在bar函数里调用时间函数?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个函数:专门设定时间:

import time
def show_time(func):
  start_time=time.time()
  func()
  end_time=time.time()
  print('spend %s'%(end_time-start_time))
def foo():
  print('hello foo')
  time.sleep(3)
show_time(foo)

但是这样的话,你基础平台的函数修改了名字,容易被业务线的人投诉的,因为我们每次都要将一个函数作为参数传递给show_time函数。而且这种方式已经破坏了原有的代码逻辑结构,之前执行业务逻辑时,执行运行foo(),但是现在不得不改成show_time(foo)。那么有没有更好的方式的呢?当然有,答案就是装饰器。

def show_time(f):
  def inner():
    start = time.time()
    f()
    end = time.time()
    print('spend %s'%(end - start))
  return inner
@show_time #foo=show_time(f)
def foo():
  print('foo...')
  time.sleep(1)
foo()
def bar():
  print('bar...')
  time.sleep(2)
bar()

输出结果:

foo...
spend 1.0005607604980469
bar...

函数show_time就是装饰器,它把真正的业务方法f包裹在函数里面,看起来像foo被上下时间函数装饰了。在这个例子中,函数进入和退出时 ,被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。

@符号是装饰器的语法糖,在定义函数的时候使用,避免再一次赋值操作

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

装饰器有2个特性,一是可以把被装饰的函数替换成其他函数, 二是可以在加载模块时候立即执行

def decorate(func):
  print('running decorate', func)
  def decorate_inner():
    print('running decorate_inner function')
    return func()
  return decorate_inner
@decorate
def func_1():
  print('running func_1')
if __name__ == '__main__':
  print(func_1)
  #running decorate <function func_1 at 0x000001904743DEA0>
  # <function decorate.<locals>.decorate_inner at 0x000001904743DF28>
  func_1()
  #running decorate_inner function
  # running func_1

通过args 和 *kwargs 传递被修饰函数中的参数

def decorate(func):
  def decorate_inner(*args, **kwargs):
    print(type(args), type(kwargs))
    print('args', args, 'kwargs', kwargs)
    return func(*args, **kwargs)
  return decorate_inner
@decorate
def func_1(*args, **kwargs):
  print(args, kwargs)
if __name__ == '__main__':
  func_1('1', '2', '3', para_1='1', para_2='2', para_3='3')
#返回结果
#<class 'tuple'> <class 'dict'>
# args ('1', '2', '3') kwargs {'para_1': '1', 'para_2': '2', 'para_3': '3'}
# ('1', '2', '3') {'para_1': '1', 'para_2': '2', 'para_3': '3'}

带参数的被装饰函数 

import time
# 定长
def show_time(f):
  def inner(x,y):
    start = time.time()
    f(x,y)
    end = time.time()
    print('spend %s'%(end - start))
  return inner
@show_time
def add(a,b):
  print(a+b)
  time.sleep(1)
add(1,2)

不定长

import time
#不定长
def show_time(f):
  def inner(*x,**y):
    start = time.time()
    f(*x,**y)
    end = time.time()
    print('spend %s'%(end - start))
  return inner
@show_time
def add(*a,**b):
  sum=0
  for i in a:
    sum+=i
  print(sum)
  time.sleep(1)
add(1,2,3,4)

带参数的装饰器

在上面的装饰器调用中,比如@show_time,该装饰器唯一的参数就是执行业务的函数。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。

import time
def time_logger(flag=0):
  def show_time(func):
    def wrapper(*args, **kwargs):
      start_time = time.time()
      func(*args, **kwargs)
      end_time = time.time()
      print('spend %s' % (end_time - start_time))
      if flag:
        print('将这个操作的时间记录到日志中')
    return wrapper
  return show_time
@time_logger(flag=1)
def add(*args, **kwargs):
  time.sleep(1)
  sum = 0
  for i in args:
    sum += i
  print(sum)
add(1, 2, 5)

@time_logger(flag=1) 做了两件事:

(1)time_logger(1):得到闭包函数show_time,里面保存环境变量flag

(2)@show_time   :add=show_time(add)

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

叠放装饰器

执行顺序是什么

如果一个函数被多个装饰器修饰,其实应该是该函数先被最里面的装饰器修饰后(下面例子中函数main()先被inner装饰,变成新的函数),变成另一个函数后,再次被装饰器修饰

def outer(func):
  print('enter outer', func)
  def wrapper():
    print('running outer')
    func()
  return wrapper
def inner(func):
  print('enter inner', func)
  def wrapper():
    print('running inner')
    func()
  return wrapper
@outer
@inner
def main():
  print('running main')
if __name__ == '__main__':
  main()
#返回结果
# enter inner <function main at 0x000001A9F2BCDF28>
# enter outer <function inner.<locals>.wrapper at 0x000001A9F2BD5048>
# running outer
# running inner
# running main

类装饰器

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

import time
class Foo(object):
  def __init__(self, func):
    self._func = func
  def __call__(self):
    start_time=time.time()
    self._func()
    end_time=time.time()
    print('spend %s'%(end_time-start_time))
@Foo #bar=Foo(bar)
def bar():
  print ('bar')
  time.sleep(2)
bar()  #bar=Foo(bar)()>>>>>>>没有嵌套关系了,直接active Foo的 __call__方法

标准库中有多种装饰器

例如:装饰方法的函数有property, classmethod, staticmethod; functools模块中的lru_cache, singledispatch,  wraps 等等

from functools import lru_cache
from functools import singledispatch
from functools import wraps

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

def foo():
  print("hello foo")
print(foo.__name__)# foo
def logged(func):
  def wrapper(*args, **kwargs):
    print (func.__name__ + " was called")
    return func(*args, **kwargs)
  return wrapper
@logged
def cal(x):
  resul=x + x * x
  print(resul)
cal(2)
#6
#cal was called
print(cal.__name__)# wrapper
print(cal.__doc__)#None
#函数f被wrapper取代了,当然它的docstring,__name__就是变成了wrapper函数的信息了。

好在我们有functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器函数中,这使得装饰器函数也有和原函数一样的元信息了。

from functools import wraps
def logged(func):
  @wraps(func)
  def wrapper(*args, **kwargs):
    print(func.__name__ + " was called")
    return func(*args, **kwargs)
  return wrapper
@logged
def cal(x):
  return x + x * x
print(cal.__name__) # cal

使用装饰器会产生我们可能不希望出现的副作用, 例如:改变被修饰函数名称,对于调试器或者对象序列化器等需要使用内省机制的那些工具,可能会无法正常运行;

其实调用装饰器后,会将同一个作用域中原来函数同名的那个变量(例如下面的func_1),重新赋值为装饰器返回的对象;使用@wraps后,会把与内部函数(被修饰函数,例如下面的func_1)相关的重要元数据全部复制到外围函数(例如下面的decorate_inner)

from functools import wraps
def decorate(func):
  print('running decorate', func)
  @wraps(func)
  def decorate_inner():
    print('running decorate_inner function', decorate_inner)
    return func()
  return decorate_inner
@decorate
def func_1():
  print('running func_1', func_1)
if __name__ == '__main__':
  func_1()
#输出结果
#running decorate <function func_1 at 0x0000023E8DBD78C8>
# running decorate_inner function <function func_1 at 0x0000023E8DBD7950>
# running func_1 <function func_1 at 0x0000023E8DBD7950>

关于Python相关内容感兴趣的读者可查看本站专题:《Python函数使用技巧总结》、《Python面向对象程序设计入门与进阶教程》、《Python数据结构与算法教程》、《Python字符串操作技巧汇总》、《Python编码操作技巧总结》及《Python入门与进阶经典教程》

希望本文所述对大家Python程序设计有所帮助。

Python 相关文章推荐
windows系统中python使用rar命令压缩多个文件夹示例
May 06 Python
Python命令行参数解析模块getopt使用实例
Apr 13 Python
Python绑定方法与非绑定方法详解
Aug 18 Python
python编程培训 python培训靠谱吗
Jan 17 Python
详解Python 4.0 预计推出的新功能
Jul 26 Python
python打造爬虫代理池过程解析
Aug 15 Python
python3实现的zip格式压缩文件夹操作示例
Aug 17 Python
浅谈Python3 numpy.ptp()最大值与最小值的差
Aug 24 Python
tensorflow如何批量读取图片
Aug 29 Python
Python库skimage绘制二值图像代码实例
Apr 10 Python
python 获取字典键值对的实现
Nov 12 Python
为了顺利买到演唱会的票用Python制作了自动抢票的脚本
Oct 16 Python
python word转pdf代码实例
Aug 16 #Python
django 快速启动数据库客户端程序的方法示例
Aug 16 #Python
djano一对一、多对多、分页实例代码
Aug 16 #Python
python try except返回异常的信息字符串代码实例
Aug 15 #Python
python 多进程共享全局变量之Manager()详解
Aug 15 #Python
使用Python调取任意数字资产钱包余额功能
Aug 15 #Python
centos7之Python3.74安装教程
Aug 15 #Python
You might like
PHP学习笔记之一
2011/01/17 PHP
Yii分页用法实例详解
2014/12/04 PHP
Redis构建分布式锁
2017/03/28 PHP
thinkphp5.1框架模板赋值与变量输出示例
2020/05/25 PHP
jquery animate图片模向滑动示例代码
2011/01/26 Javascript
js实现拖拽 闭包函数详细介绍
2012/11/25 Javascript
jQuery.Uploadify插件实现带进度条的批量上传功能
2016/06/08 Javascript
jQuery实现联动下拉列表查询框
2017/01/04 Javascript
vue日期组件 支持vue1.0和2.0
2017/01/09 Javascript
Angular2-primeNG文件上传模块FileUpload使用详解
2017/01/14 Javascript
jQuery文字轮播特效
2017/02/12 Javascript
实例详解JSON取值(key是中文或者数字)方式
2017/08/24 Javascript
浅谈在vue中使用mint-ui swipe遇到的问题
2018/09/27 Javascript
动态内存分配导致影响Javascript性能的问题
2018/12/18 Javascript
实例分析编写vue组件方法
2019/02/12 Javascript
在Python的gevent框架下执行异步的Solr查询的教程
2015/04/16 Python
python实现类的静态变量用法实例
2015/05/08 Python
独特的python循环语句
2016/11/20 Python
python使用正则表达式匹配字符串开头并打印示例
2017/01/11 Python
python清除字符串前后空格函数的方法
2018/10/21 Python
pyqt 实现QlineEdit 输入密码显示成圆点的方法
2019/06/24 Python
详细介绍Python进度条tqdm的使用
2019/07/31 Python
python实现用户名密码校验
2020/03/18 Python
python 使用csv模块读写csv格式文件的示例
2020/12/02 Python
HTML5视频支持检测(检查浏览器是否支持视频播放)
2013/06/08 HTML / CSS
丝芙兰新加坡官网:Sephora新加坡
2018/12/04 全球购物
MaBelle玛贝尔香港官网:香港钻饰连锁店
2019/09/09 全球购物
美国最大的购物网站:Amazon.com(亚马逊美国)
2020/05/23 全球购物
大学生职业生涯规划范文——找准自我,定位人生
2014/01/23 职场文书
双创工作实施方案
2014/03/26 职场文书
会计专业应届生自荐信
2014/06/28 职场文书
感谢信范文大全
2015/01/23 职场文书
小学六年级毕业感言
2015/07/30 职场文书
2016大学先进团支部事迹材料
2016/03/01 职场文书
如何用Navicat操作MySQL
2021/05/12 MySQL
浅析Python中的套接字编程
2021/06/22 Python