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装饰器初探(推荐)
Jul 21 Python
使用Python脚本实现批量网站存活检测遇到问题及解决方法
Oct 11 Python
Python创建普通菜单示例【基于win32ui模块】
May 09 Python
Python实现随机创建电话号码的方法示例
Dec 07 Python
python多进程读图提取特征存npy
May 21 Python
Django项目创建到启动详解(最全最详细)
Sep 07 Python
Python 解码Base64 得到码流格式文本实例
Jan 09 Python
在Tensorflow中查看权重的实现
Jan 24 Python
Python作用域与名字空间原理详解
Mar 21 Python
Python爬虫JSON及JSONPath运行原理详解
Jun 04 Python
.img/.hdr格式转.nii格式的操作
Jul 01 Python
Pytorch 图像变换函数集合小结
Feb 01 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
DIY实用性框形天线
2021/03/02 无线电
如何突破PHP程序员的技术瓶颈分析
2011/07/17 PHP
php静态文件生成类实例分析
2015/01/03 PHP
PHPCrawl爬虫库实现抓取酷狗歌单的方法示例
2017/12/21 PHP
ThinkPHP框架中使用Memcached缓存数据的方法
2018/03/31 PHP
ThinkPHP 5.1 跨域配置方法
2019/10/11 PHP
Aster vs Newbee BO5 第三场2.19
2021/03/10 DOTA
查询绑定数据岛的表格中的文本并修改显示方式的js代码
2009/12/15 Javascript
js下判断 iframe 是否加载完成的完美方法
2010/10/26 Javascript
扩展jquery实现客户端表格的分页、排序功能代码
2011/03/16 Javascript
JavaScript在XHTML中的用法详解
2013/04/11 Javascript
js控制frameSet示例
2013/09/10 Javascript
实例说明为什么不要行内使用javascript
2014/04/18 Javascript
JQuery 控制内容长度超出规定长度显示省略号
2014/05/23 Javascript
JS获得图片alt信息的方法
2015/04/01 Javascript
jquery实现的V字形显示效果代码
2015/10/27 Javascript
javascript 的变量、作用域和内存问题
2017/04/19 Javascript
基于Bootstrap模态对话框只加载一次 remote 数据的解决方法
2017/07/09 Javascript
详解在vue-test-utils中mock全局对象
2018/11/07 Javascript
layui 实现自动选择radio单选框(checked)的方法
2019/09/03 Javascript
javascript+Canvas实现画板功能
2020/06/23 Javascript
关于Python中异常(Exception)的汇总
2017/01/18 Python
python如何把嵌套列表转变成普通列表
2018/03/20 Python
Python3 sys.argv[ ]用法详解
2019/10/24 Python
python实现人机猜拳小游戏
2020/02/03 Python
最新版 Windows10上安装Python 3.8.5的步骤详解
2020/11/28 Python
Python制作简单的剪刀石头布游戏
2020/12/10 Python
使用Python制作一盏 3D 花灯喜迎元宵佳节
2021/02/26 Python
荷兰皇家航空公司官方网站:KLM Royal Dutch Airlines
2017/12/07 全球购物
介绍一下linux的文件系统
2015/10/06 面试题
职称自我鉴定
2013/10/15 职场文书
2014年青年志愿者工作总结
2014/12/09 职场文书
优秀共产党员推荐材料
2014/12/18 职场文书
一篇文章看懂MySQL主从复制与读写分离
2021/11/07 MySQL
python中数组和列表的简单实例
2022/03/25 Python
4种方法python批量修改替换列表中元素
2022/04/07 Python