装饰器(decorator)

理解了上一章的闭包之后,装饰器就是闭包的一种应用,只是外部函数的参数传入的不是普通的变量类型,而是传入一个函数名。装饰器一般用于:不修改被装饰函数(即外部函数传入的参数)内部代码的情况下,对对装饰函数功能的新增或者拓展,比如,想知道某一个函数总共运行了多长时间,可以加一个装饰器,记录该函数在被调用前后的当前时间,再相减得到程序的运行时间,再比如在调用某个程序前后打印一些日志信息,再比如在调用某个程序前增加一些权限验证或者数据验证等。当然这些功能,直接写在函数调用的前后也可以实现,但是如果有n个函数都需要这个功能,那么就需要写上多遍,维护起来比较麻烦。这里给出一个简单的装饰器的例子,在调用函数test1时在原来的打印功能前新增一个权限验证的功能:

def set_func(func):
def cal_func():
print('--------新增权限验证功能-------')
func()
return cal_func @set_func
def test1():
print('-------调用了test1--------') test1() # 运行结果为:

--------新增权限验证功能-------
-------调用了test1--------

装饰器的手动实现过程(装饰器原理)

def set_func(func):
def cal_func():
print('--------进行数据验证-------')
print('此时变量func指向:', func)
func()
return cal_func def test1():
print('-------调用了test1--------') print('装饰前,test1指向:', test1)
print('1.新建变量ret接收set_func的返回值')
ret = set_func(test1)
print('2.此时ret指向:', ret)
print('3.调用ret')
ret() # 运行结果为:
装饰前,test1指向: <function test1 at 0x000001F2220BA1F0>
1.新建变量ret接收set_func的返回值
2.此时ret指向: <function set_func.<locals>.cal_func at 0x000001F2220BA280>
3.调用ret
--------进行数据验证-------
此时变量func指向: <function test1 at 0x000001F2220BA1F0>
-------调用了test1--------

1、定义了一个闭包,外部函数为set_func,传入参数为func,内部函数为cal_func,外部函数返回内部函数

2、定义了一个普通函数test1,函数名即变量名,此时变量test1指向名为test1的函数的地址....BA1F0

3、调用set_func:

  3.1.将上面的test1传入,此时set_func的形参func,指向了实参test1的指向,即....BA1F0

  3.2.定义了一个名为cal_func的函数,并创建其对应的内存地址,并将其指向(或引用)返回给变量ret,即ret指向了名为cal_func的函数的地址BA280

4、调用ret函数,即执行名为cal_func内部中的代码:

  4.1.打印数据验证,打印func的指向

  4.2.调用func(),因为func指向的是名为test1的函数的地址,因此执行test1中的代码,打印‘-------调用了test1--------’

至此运行完毕,成功在打印调用test1之前打印出了数据验证的代码。但是本来我们调用的是test1,现在为了加上数据验证功能,需要让改成调用ret,这样还是修改了原来的代码。接下来将程序进一步修改,我们之前使用的变量名为ret的变量接收set_func的返回值,我们也可以把ret换成aaa、bbb、foo都可以,现在我们把变量名test1,在上个代码版本上添加 ‘===’分割线 后面的代码

def set_func(func):
def cal_func():
print('--------进行数据验证-------')
print('此时变量func指向:', func)
func()
return cal_func def test1():
print('-------调用了test1--------') print('装饰前,test1指向:', test1)
print('手动实现装饰器:')
print('1.新建变量ret接收set_func的返回值')
ret = set_func(test1)
print('2.此时ret指向:', ret)
print('3.调用ret')
ret()
print('=' * 30, '我是分割线', '=' * 30)
print('1.将上述ret变量名换成test1')
test1 = set_func(test1)
print('2.此时test1指向:', test1)
print('3.调用test')
test1()

运行结果为:

装饰前,test1指向: <function test1 at 0x0000019042CA0280>
手动实现装饰器:
1.新建变量ret接收set_func的返回值
2.此时ret指向: <function set_func.<locals>.cal_func at 0x0000019042CA0310>
3.调用ret
--------进行数据验证-------
此时变量func指向: <function test1 at 0x0000019042CA0280>
-------调用了test1--------
============================== 我是分割线 ==============================
1.将上述ret变量名换成test1
2.此时test1指向: <function set_func.<locals>.cal_func at 0x0000019042CA03A0>
3.调用test
--------进行数据验证-------
此时变量func指向: <function test1 at 0x0000019042CA0280>
-------调用了test1--------

1、在调用set_func函数时,返回cal_func的地址引用给变量test1,即在调用前,原来的变量test1指向的是函数名为test1的地址...CA0280(注意这里程序每次运行时,给同一个函数名创建的内存地址可能会存在不一样,所以这里地址和上面程序的输出结果打印的地址不一样),现在调用后,将变量的test1的指向变成了指向cal_func的地址....CA03A0。而每次调用set_func时都会让形参func指向函数test1的地址...CA0280,因此虽然变量test1不再指向原函数test1的地址,但是一定会有一个形参指向函数test1的地址并保存了起来,这就是前面说的闭包,调用闭包外部函数时,会将传入的参数进行保存,并在调用内部函数时使用到保存的外部参数。

2、set_func返回函数cal_func的引用给变量test1后,再次调用test1(),即实际调用的是cal_func方法,在cal_fun方法中,打印数据验证,并调用func(),func指向的是函数test1,即调用函数test1,打印test中的内容

上述将变量test1由之前指向函数test1,改变为指向set_func的返回值即cal_func的过程,对应代码为test1 = set_func(test1),即为装饰的过程。为了简写,而不需要每次都写上test1 = set_func(test1),就可以在函数test1定义前加上@set_func,即@set_func等价于test1 = set_func(test1)

完善装饰器

1.给装饰器内部方法添加参数和返回值

因为一个装饰器可能对多个函数进行装饰,而这多个函数可能参数不一样,或者是否有返回值也不一样,为了使装饰器适配所有的被装饰函数,需要给装饰器添加参数和返回值,如:

# 装饰器为了满足被装饰函数的所有参数和返回值
# 1.需要在cal_func中写上不定长参数*args和**kwargs
# 1.1 注意使用的参数名就叫做args和kwargs,而不是*args和**kwargs
# 这里参数中写的*和**是为了让python解释器将多余的参数分为元组tuple和字典dic
# 1.2 而下面调用func传入的参数时,也加上了*和**,这里*和**的作用是将args和kwargs拆包
# 使传入的参数拆成在同一个维度上面,相当于拆成func('a', 'b', 'c', d=1, e='2')
# 如果调用func传入时参数不带*,则相当于传入的参数为func(('a', 'b', 'c'), {'d': 1, 'e': '2'})
# 这样会导致解释器解析func的参数时,认为传入的是两个值,一个是元组,一个是字典
# 然后在func中处理参数时,将第一个元组通过para接收,第二个字典通过*args接收,导致最终结果与预期不一样
# 2.在cal_func中调用传入的func前,添加return,这样如果被装饰函数也有return的话,可以将最终的结果也return出来,如test2 def set_func(func):
def cal_func(*args, **kwargs):
print('------数据验证------')
print('args', args)
print('kwargs', kwargs)
return func(args, kwargs)
return cal_func @set_func
def test1(para, *args, **kwargs):
print('------调用test1------')
print('------para------', para)
print('------*args------', args)
print('------*kwargs------', kwargs) @set_func
def test2(para, *args, **kwargs):
print('------调用test2------')
print('------para------', para)
print('------*args------', args)
print('------*kwargs------', kwargs)
return 'ok...' test1(1)
print('=' * 30, '我是分割线', '=' * 30) test1('a')
print('=' * 30, '我是分割线', '=' * 30) test1('a', 'b', 'c')
print('=' * 30, '我是分割线', '=' * 30) test1('a', 'b', 'c', d=1, e='')
print('=' * 30, '我是分割线', '=' * 30) print(test2('a', 'b', 'c', d=1, e=''))

运行结果为:

------数据验证------
args (1,)
kwargs {}
------调用test1------
------para------ (1,)
------*args------ ({},)
------*kwargs------ {}
============================== 我是分割线 ==============================
------数据验证------
args ('a',)
kwargs {}
------调用test1------
------para------ ('a',)
------*args------ ({},)
------*kwargs------ {}
============================== 我是分割线 ==============================
------数据验证------
args ('a', 'b', 'c')
kwargs {}
------调用test1------
------para------ ('a', 'b', 'c')
------*args------ ({},)
------*kwargs------ {}
============================== 我是分割线 ==============================
------数据验证------
args ('a', 'b', 'c')
kwargs {'d': 1, 'e': ''}
------调用test1------
------para------ ('a', 'b', 'c')
------*args------ ({'d': 1, 'e': ''},)
------*kwargs------ {}
============================== 我是分割线 ==============================
------数据验证------
args ('a', 'b', 'c')
kwargs {'d': 1, 'e': ''}
------调用test2------
------para------ ('a', 'b', 'c')
------*args------ ({'d': 1, 'e': ''},)
------*kwargs------ {}
ok...

2.给装饰器添加参数

目前的装饰器在装饰不同的函数时,前面的装饰的内容都是一致的,那么在同一个装饰器中能不能给不同的被装饰函数添加不同的功能呢?比如在装饰test1时,添加test1的数据验证,装饰test2时,添加test2的装饰验证。

目前装饰器外层函数的参数是固定的,用来接收被装饰的函数,内层函数的参数也是固定的,用来接收传入被装饰函数的参数。那么就需要在现有装饰器外再套一层parent_func函数,其中设置一个参数用来接收是哪一个被装饰函数的名字,即:

def parent_func(name):
def set_func(func):
def cal_func(*args, **kwargs):
print('------%s的数据验证------' % name)
return func(args, kwargs)
return cal_func
return set_func @parent_func('test1')
def test1(para, *args, **kwargs):
print('------调用test1------') @parent_func('test2')
def test2(para, *args, **kwargs):
print('------调用test2------')
return 'ok...' test1(1)
print('=' * 30, '我是分割线', '=' * 30)
print(test2('a', 'b', 'c', d=1, e='')) # 运行结果为:
------test1的数据验证------
------调用test1------
============================== 我是分割线 ==============================
------test2的数据验证------
------调用test2------
ok...

1、定义再添加一层函数parent_fun,接收一个或多个参数,看具体业务需求,该parent_fun函数返回原来的装饰器函数

2、将原来的@set_func改成@parent_func(xxx),可以理解为当运行到parent_func('test1')这行代码时,即会调用parent_func函数,返回原来的那个装饰器,再让原来的装饰器对函数进行装饰

多个装饰器对同一个函数进行装饰

有时候我们需要对一个函数添加好几个装饰器呢,如一个是权限验证的装饰器,一个是数据验证装饰器等,如:

def set_func1(func):
print('----1.权限验证开始装饰----') def cal_func(*args, **kwargs):
print('------1.权限验证------')
return func(args, kwargs)
return cal_func def set_func2(func):
print('----2.数据验证开始装饰----') def cal_func(*args, **kwargs):
print('------2.数据验证------')
return func(args, kwargs)
return cal_func @set_func1
@set_func2
def test1(para, *args, **kwargs):
print('------调用test1------') print('开始调用test1')
test1(1)

运行结果:

----2.数据验证开始装饰----
----1.权限验证开始装饰----
开始调用test1
------1.权限验证------
------2.数据验证------
------调用test1------

1、创建了两个装饰器,set_func1和set_func2,对test1装饰时,@set_func1写在@set_func2前面

2、根据打印结果可以发现两个现象:

  2.1.'开始装饰'的打印在'开始调用'之前,说明了装饰这个过程并不是在调用被装饰函数才进行装饰,而是在运行到@xxxxx时就开始装饰了

  2.2.在装饰的时候,是从下往上顺序装饰的,而调用时,是从上往下调用的装饰器,可以这么理解:解释器从上往下执行,当执行到第一个装饰器@set_func1时,发现下面一行代码并不是定义一个函数,则跳过改行代码,继续往下执行,执行到@set_func2,发现set_func2的下一行是定义一个函数,那么就开始set_func2的装饰,装饰完之后,再回到上一行开始set_func1对刚才装饰的结果再进行一层装饰,这样的结果就是最外层是set_func1,中间层是set_func2,最里层是test1。那么最后在调用test1的时候,从外往里执行,就先执行的set_func1再执行set_func2,最后执行test1

装饰器会改变原函数变量的一些函数属性

使用了装饰器后,由于指向原函数的变量指向了装饰器内部定义的函数,因此如果调用原函数变量的__name__和__doc__等方法时,得到的是装饰器内部函数的值,如:

def set_func(func):
def cal_func():
"""this is cal_func doc"""
print('cal_func')
return cal_func @set_func
def test():
"""this is test doc"""
print('test') print(test.__name__)
print(test.__doc__) # 运行结果
# cal_func
# this is cal_func doc

可以看出打印出来的是内部函数cal_func的属性,即写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了。因此为了解决这个问题,任何时候定义装饰器的时候,都应该使用 functools 库中的 @wraps 装饰器来装饰内部函数。例如:

from functools import wraps

def set_func(func):
@wraps(func)
def cal_func():
"""this is cal_func doc"""
print('cal_func')
return cal_func @set_func
def test():
"""this is test doc"""
print('test') print(test.__name__)
print(test.__doc__) # 运行结果
# test
# this is test doc

python-闭包和装饰器-02-装饰器(decorator)的更多相关文章

  1. Python闭包及装饰器

    Python闭包 先看一个例子: def outer(x): def inner(y): return x+y return innder add = outer(8) print add(6) 我们 ...

  2. python闭包和装饰器

    本文目录: 1. 闭包的解析和用法 2. 函数式装饰器 3. 类装饰器 一.闭包 闭包是一种函数,从形式上来说是函数内部定义(嵌套)函数,实现函数的扩展.在开发过程中,考虑到兼容性和耦合度问题,如果想 ...

  3. python 闭包和装饰器

    python 闭包和装饰器 一.闭包闭包:外部函数FunOut()里面包含一个内部函数FunIn(),并且外部函数返回内部函数的对象FunIn,内部函数存在对外部函数的变量的引用.那么这个内部函数Fu ...

  4. python闭包和装饰器(转)

    一.python闭包 1.内嵌函数 >>> def func1(): ... print ('func1 running...') ... def func2(): ... prin ...

  5. 详解Python闭包,装饰器及类装饰器

    在项目开发中,总会遇到在原代码的基础上添加额外的功能模块,原有的代码也许是很久以前所写,为了添加新功能的代码块,您一般还得重新熟悉源代码,稍微搞清楚一点它的逻辑,这无疑是一件特别头疼的事情.今天我们介 ...

  6. 高逼格利器之Python闭包与装饰器

    生活在魔都的小明,终于攒够了首付,在魔都郊区买了一套房子:有一天,小明踩了狗屎,中了一注彩票,得到了20w,小明很是欢喜,于是想干脆用这20万来装修房子吧(decoration): 整个装修过程,小明 ...

  7. Python 闭包、迭代器、生成器、装饰器

    Python 闭包.迭代器.生成器.装饰器 一.闭包 闭包:闭包就是内层函数对外层函数局部变量的引用. def func(): a = "哈哈" def func2(): prin ...

  8. Python 简明教程 --- 22,Python 闭包与装饰器

    微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 当你选择了一种语言,意味着你还选择了一组技术.一个社区. 目录 本节我们来介绍闭包与装饰器. 闭包与 ...

  9. python函数之闭包函数与无参装饰器

    一.global与nonlocal #global x = 1 def f1(): global x # 声明此处是全部变量x x = 2 print(x) f1() # 调用f1后,修改了全局变量x ...

  10. python 闭包函数与装饰器

    1.什么是闭包函数 (1):什么是闭包函数: #内部函数包含对外部作用域而非全局作用域的引用, 简而言之, 闭包的特点就是内部函数引用了外部函数中的变量. 在Python中,支持将函数当做对象使用,也 ...

随机推荐

  1. 9、ssh的集成方式1

    集成方式1:核心 我们没有创建applicationContext-action.xml配置文件,在该配置文件里面让Spring去管理我们的AddUserAction,但是AddUserAction的 ...

  2. IDEA记坑之移动项目文件之后,import 找不到文件以及出现Cannot access的问题

    今天本想挪动下文件,使项目更加可观,易整理,但是挪动后出现各种问题,import xxx;全部飘红.部分切面还出现Cannot access:试过了重启idea,rebuild....各种方法都行不通 ...

  3. JDK8--08:Optional

    在程序运行时,空指针异常应该是最常见的异常之一,因此JDK8提供了Optional来避免空指针异常. 首先说明JDK8新增的Optional及相关方法的使用 Optional的常用操作: Option ...

  4. Android 伤敌一千自损八百之萤石摄像头集成(二)

    本章主要是结合webview展示直播 app负责配网展示设备 加载webview播放 不介绍别的只说集成,至于APP_KEY.accessToken怎么获取的不予解释,官网都有 获取WiFi名称 这里 ...

  5. day01微信小程序

    一.基本概要 1.一个程序接口,可以集成很多功能,也就是在程序上再次开发 腾讯:微信+小程序 阿里:支付宝  +小程序 小程序的使用量很多 2.为什么要微信小程序? 1.微信用户群体大 2.容易推广, ...

  6. 数据解析_xpath

    重点推荐这种解析方式,xpath是最常用且最便捷高效的一种解析方式,通用性 1.解析原理 1.实例化一个etree的对象,且需要将被解析的页面源码数据加载到改对象中. 2.调用etree对象中的xpa ...

  7. 关于位图数据位和系统管理区大小-P6

    文章目录 1 背景 2 验证 2.1 环境信息 2.2 创建表空间tbs1 2.3 创建表段并拓展至16个区 2.4 查看3号位图块信息 2.5 拓展16号区 2.6 查看3号位图块信息 1 背景 V ...

  8. A Simple Problem,题解

    题目: 分析: 看到式子,推一推其实就是(y+x)*(y-x)=n,显然可以根号n的枚举,判断一下合不合法直接出结果,然后就是代码.注意x!=0. #include <cstdio> #i ...

  9. 记一次在Grafana中使用Worldmap Panel的经历

    背景 因与工作相关,以下内容皆做了脱敏处理 主要的需求是要根据地理位置查看可视化的数据. 安装及创建 安装命令来源于官网 grafana-cli plugins install grafana-wor ...

  10. djangorestframework学习1-通过HyperlinkedModelSerializer,ModelViewSet,routers编写第一个接口

    前提首先安装了django,安装方式:pip install django 1. djangorestftamework安装: pip install djangorestframework 2. 创 ...