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程序实例(通讯录)
Nov 29 Python
以windows service方式运行Python程序的方法
Jun 03 Python
Python单链表简单实现代码
Apr 27 Python
深入理解Python3中的http.client模块
Mar 29 Python
给你选择Python语言实现机器学习算法的三大理由
Nov 15 Python
flask框架中勾子函数的使用详解
Aug 01 Python
Python 3.8新特征之asyncio REPL
May 28 Python
python简单实现矩阵的乘,加,转置和逆运算示例
Jul 10 Python
python利用datetime模块计算程序运行时间问题
Feb 20 Python
Python3如何在Windows和Linux上打包
Feb 25 Python
Pytest如何使用skip跳过执行测试
Aug 13 Python
Python2及Python3如何实现兼容切换
Sep 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
用Simple Excel导出xls实现方法
2012/12/06 PHP
获取PHP警告错误信息的解决方法
2013/06/03 PHP
PHP文件缓存smarty模板应用实例分析
2016/02/26 PHP
php+redis消息队列实现抢购功能
2018/02/08 PHP
php分享朋友圈的实现代码
2019/02/18 PHP
PHP使用Http Post请求发送Json对象数据代码解析
2020/07/16 PHP
PHP常用header头定义代码示例汇总
2020/08/29 PHP
js unicode 编码解析关于数据转换为中文的两种方法
2014/04/21 Javascript
javascript实用方法总结
2015/02/06 Javascript
jQuery实现带延迟效果的滑动菜单代码
2015/09/02 Javascript
jQuery mobile类库使用时加载导航历史的方法简介
2015/12/04 Javascript
JS判断日期格式是否合法的简单实例
2016/07/11 Javascript
AngularJS中isolate scope的用法分析
2016/11/22 Javascript
数组Array的一些方法(总结)
2017/02/17 Javascript
Vue.directive 自定义指令的问题小结
2018/03/04 Javascript
实例详解Vue项目使用eslint + prettier规范代码风格
2018/08/20 Javascript
vue动态注册组件实例代码详解
2019/05/30 Javascript
微信小程序中的video视频实现 自定义播放按钮、封面图、视频封面上文案
2020/01/02 Javascript
React Native中ScrollView组件轮播图与ListView渲染列表组件用法实例分析
2020/01/06 Javascript
JavaScript实现轮播图片完整代码
2020/03/07 Javascript
vue大型项目之分模块运行/打包的实现
2020/09/21 Javascript
解决Vue项目中tff报错的问题
2020/10/21 Javascript
Vue3+elementui plus创建项目的方法
2020/12/01 Vue.js
Python实现简单HTML表格解析的方法
2015/06/15 Python
python逆向入门教程
2018/01/15 Python
浅谈python numpy中nonzero()的用法
2018/04/02 Python
Python Series从0开始索引的方法
2018/11/06 Python
python3 实现的对象与json相互转换操作示例
2019/08/17 Python
Python的collections模块真的很好用
2021/03/01 Python
解析HTML5中的新功能本地存储localStorage
2016/03/01 HTML / CSS
HelloFresh澳大利亚:订购你的美味食品盒、健康餐食
2018/03/28 全球购物
暑假社会实践心得体会
2014/09/02 职场文书
2014年民警工作总结
2014/11/25 职场文书
学校实习推荐信
2015/03/27 职场文书
财务部岗位职责范本
2015/04/14 职场文书
刑事辩护词范文
2015/05/21 职场文书