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单链表的简单实现方法
Sep 23 Python
简单介绍Python中的JSON使用
Apr 28 Python
python中for语句简单遍历数据的方法
May 07 Python
Python程序中的观察者模式结构编写示例
May 27 Python
python语音识别实践之百度语音API
Aug 30 Python
python可视化实现代码
Jan 15 Python
Python安装与基本数据类型教程详解
May 29 Python
Django框架 Pagination分页实现代码实例
Sep 04 Python
python 模拟创建seafile 目录操作示例
Sep 26 Python
python几种常用功能实现代码实例
Dec 25 Python
Python实现我的世界小游戏源代码
Mar 02 Python
使用python求解迷宫问题的三种实现方法
Mar 17 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代码(抓取网页中的邮箱地址)
2012/07/17 PHP
php正则表达式学习笔记
2015/11/13 PHP
PHP socket 模拟POST 请求实例代码
2016/07/18 PHP
thinkphp实现把数据库中的列的值存到下拉框中的方法
2017/01/20 PHP
PHP设计模式之组合模式定义与应用示例
2020/02/01 PHP
php设计模式之模板模式实例分析【星际争霸游戏案例】
2020/03/24 PHP
通过上下左右键和回车键切换光标实现代码
2013/03/08 Javascript
JQuery 操作/获取table具体代码
2013/06/13 Javascript
jQuery获取选中内容及设置元素属性的方法
2014/07/09 Javascript
js数组常见操作及数组与字符串相互转化实例详解
2015/11/10 Javascript
详解JavaScript语言的基本语法要求
2015/11/20 Javascript
jQuery实现点击水纹波动动画
2016/04/10 Javascript
全面解析Bootstrap中nav、collapse的使用方法
2016/05/22 Javascript
深入理解jQuery之事件移除
2016/06/02 Javascript
Javascript中apply、call、bind的巧妙使用
2016/08/18 Javascript
JQuery遍历元素的父辈和祖先的方法
2016/09/18 Javascript
jQuery图片切换动画特效
2016/11/02 Javascript
vuejs2.0实现分页组件使用$emit进行事件监听数据传递的方法
2017/02/22 Javascript
VUE中v-model和v-for指令详解
2017/06/23 Javascript
JavaScript简单实现合并两个Json对象的方法示例
2017/10/16 Javascript
JavaScript数据结构之双向链表定义与使用方法示例
2017/10/27 Javascript
cnpm加速Angular项目创建的方法
2018/09/07 Javascript
element el-tree组件的动态加载、新增、更新节点的实现
2020/02/27 Javascript
jQuery实现颜色打字机的完整代码
2020/03/19 jQuery
python使用MySQLdb访问mysql数据库的方法
2015/08/03 Python
python仿抖音表白神器
2019/04/08 Python
pytorch中如何使用DataLoader对数据集进行批处理的方法
2019/08/06 Python
python实现静态服务器
2019/09/05 Python
Python实现中值滤波去噪方式
2019/12/18 Python
Python中flatten( ),matrix.A用法说明
2020/07/05 Python
html5 css3 动态气泡按钮实例演示
2012/12/02 HTML / CSS
领导班子民主生活会整改措施(工商局)
2014/09/21 职场文书
大学生暑假实习总结
2015/07/13 职场文书
致接力运动员加油稿
2015/07/21 职场文书
Python入门之基础语法详解
2021/05/11 Python
MySQL解决Navicat设置默认字符串时的报错问题
2022/06/16 MySQL