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 相关文章推荐
python logging类库使用例子
Nov 22 Python
Python每天必学之bytes字节
Jan 28 Python
python实现excel读写数据
Mar 02 Python
解决Django删除migrations文件夹中的文件后出现的异常问题
Aug 31 Python
Python发送邮件的实例代码讲解
Oct 16 Python
python列表切片和嵌套列表取值操作详解
Feb 27 Python
PyQt5事件处理之定时在控件上显示信息的代码
Mar 25 Python
Keras-多输入多输出实例(多任务)
Jun 22 Python
如何使用python socket模块实现简单的文件下载
Sep 04 Python
Python常用数字处理基本操作汇总
Sep 10 Python
详解python程序中的多任务
Sep 16 Python
python实现学生信息管理系统(精简版)
Nov 27 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提取中文首字母
2008/04/09 PHP
PHP7.0版本备注
2015/07/23 PHP
IE 下的只读 innerHTML
2009/08/21 Javascript
javascript document.compatMode兼容性
2010/02/23 Javascript
javascript定时变换图片实例代码
2013/03/17 Javascript
使用GruntJS构建Web程序之构建篇
2014/06/04 Javascript
浅谈javascript的分号的使用
2015/05/12 Javascript
JavaScript小技巧整理
2015/12/30 Javascript
node.js 动态执行脚本
2016/06/02 Javascript
jQuery EasyUI框架中的Datagrid数据表格组件结构详解
2016/06/09 Javascript
Node.js开启Https的实践详解
2016/10/25 Javascript
浅谈JS验证表单文本域输入空格的问题
2017/02/14 Javascript
Bootstrap下拉菜单更改为悬停(hover)触发的方法
2017/05/24 Javascript
详解angular ui-grid之过滤器设置
2017/06/07 Javascript
ES6知识点整理之Proxy的应用实例详解
2019/04/16 Javascript
JQuery 实现文件下载的常用方法分析
2019/10/29 jQuery
小程序中设置缓存过期的实现方法
2020/01/14 Javascript
PyQt5利用QPainter绘制各种图形的实例
2017/10/19 Python
python3利用Dlib19.7实现人脸68个特征点标定
2018/02/26 Python
详解TensorFlow查看ckpt中变量的几种方法
2018/06/19 Python
Python IDLE清空窗口的实例
2018/06/25 Python
python绘制直线的方法
2018/06/30 Python
python模式 工厂模式原理及实例详解
2020/02/11 Python
Tensorflow中的图(tf.Graph)和会话(tf.Session)的实现
2020/04/22 Python
Python3之乱码\xe6\x97\xa0\xe6\xb3\x95处理方式
2020/05/11 Python
利用OpenCV中对图像数据进行64F和8U转换的方式
2020/06/03 Python
Wiggle美国:英国骑行、跑步、游泳、铁人三项商店
2018/10/27 全球购物
保安队长职务说明书
2014/02/23 职场文书
大型会议接待方案
2014/03/01 职场文书
讲座主持词
2014/03/20 职场文书
2014年幼儿园学期工作总结
2014/12/05 职场文书
研究生个人学年总结
2015/02/14 职场文书
计划生育工作总结2015
2015/04/03 职场文书
拯救大兵瑞恩观后感
2015/06/09 职场文书
人力资源部工作计划
2019/05/14 职场文书
Z-Order加速Hudi大规模数据集方案分析
2022/03/31 Servers