python-闭包和装饰器-02-装饰器(decorator)
装饰器(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)的更多相关文章
- Python闭包及装饰器
Python闭包 先看一个例子: def outer(x): def inner(y): return x+y return innder add = outer(8) print add(6) 我们 ...
- python闭包和装饰器
本文目录: 1. 闭包的解析和用法 2. 函数式装饰器 3. 类装饰器 一.闭包 闭包是一种函数,从形式上来说是函数内部定义(嵌套)函数,实现函数的扩展.在开发过程中,考虑到兼容性和耦合度问题,如果想 ...
- python 闭包和装饰器
python 闭包和装饰器 一.闭包闭包:外部函数FunOut()里面包含一个内部函数FunIn(),并且外部函数返回内部函数的对象FunIn,内部函数存在对外部函数的变量的引用.那么这个内部函数Fu ...
- python闭包和装饰器(转)
一.python闭包 1.内嵌函数 >>> def func1(): ... print ('func1 running...') ... def func2(): ... prin ...
- 详解Python闭包,装饰器及类装饰器
在项目开发中,总会遇到在原代码的基础上添加额外的功能模块,原有的代码也许是很久以前所写,为了添加新功能的代码块,您一般还得重新熟悉源代码,稍微搞清楚一点它的逻辑,这无疑是一件特别头疼的事情.今天我们介 ...
- 高逼格利器之Python闭包与装饰器
生活在魔都的小明,终于攒够了首付,在魔都郊区买了一套房子:有一天,小明踩了狗屎,中了一注彩票,得到了20w,小明很是欢喜,于是想干脆用这20万来装修房子吧(decoration): 整个装修过程,小明 ...
- Python 闭包、迭代器、生成器、装饰器
Python 闭包.迭代器.生成器.装饰器 一.闭包 闭包:闭包就是内层函数对外层函数局部变量的引用. def func(): a = "哈哈" def func2(): prin ...
- Python 简明教程 --- 22,Python 闭包与装饰器
微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 当你选择了一种语言,意味着你还选择了一组技术.一个社区. 目录 本节我们来介绍闭包与装饰器. 闭包与 ...
- python函数之闭包函数与无参装饰器
一.global与nonlocal #global x = 1 def f1(): global x # 声明此处是全部变量x x = 2 print(x) f1() # 调用f1后,修改了全局变量x ...
- python 闭包函数与装饰器
1.什么是闭包函数 (1):什么是闭包函数: #内部函数包含对外部作用域而非全局作用域的引用, 简而言之, 闭包的特点就是内部函数引用了外部函数中的变量. 在Python中,支持将函数当做对象使用,也 ...
随机推荐
- java 虚拟机指令重新排序
指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度.编译器.处理器也遵循这样一个目标.注意是单线程.多线程的情况下指令重排序就会给程序员带来问题. ...
- python+opencv实现图像缩放
x, y = img_.shape[0:2] img_ = cv2.resize(img_, (int(y/2), int(x/2))) 实现图像长宽缩小为原来的一半
- Apache Dubbo Provider默认反序列漏洞复现(CVE-2020-1948)
Apache Dubbo Provider默认反序列漏洞(CVE-2020-1948) 0x01 搭建漏洞环境 漏洞介绍 2020年06月23日, 360CERT监测发现Apache Dubbo 官方 ...
- valueOf()、toString()
基本上,所有JS数据类型都拥有valueOf和toString这两个方法,null除外.它们俩解决javascript值运算与显示的问题. JavaScript 的 valueOf() 方法 valu ...
- 打包发布 Qt Quick/Widgets 程序
使用的QT自带的部署工具(windeployqt.exe,路径QT安装路径),版本替换debug/release Qt Quick "C:\Qt\Qt5.8.0\5.8\mingw53_32 ...
- 聊聊Java中的异常及处理
前言 在编程中异常报错是不可避免的.特别是在学习某个语言初期,看到异常报错就抓耳挠腮,常常开玩笑说编程1分钟,改bug1小时.今天就让我们来看看什么是异常和怎么合理的处理异常吧! 异常与error介绍 ...
- Java基础Day08(多线程)
多线程 1. 线程 1.1 什么是线程: 程序中负责执行的哪个东东就叫做线程(执行路线,进程内部的执行序列或者说是进程的子任务) 多线程执行时,在栈内存中,每一个执行线程都有自己所属的栈内存空间.进行 ...
- CentOS7下普通账号通过systemctl管理服务需要输入root密码问题
问题描述: 使用普通账号test通过systemctl启动系统服务提示需要输入root密码: 解决方案: 根据上面提示得知权限由polkit进行管理,对应的是org.freedesktop.syste ...
- PHP实现邮箱验证码验证功能
*文章来源:https://blog.egsec.cn/archives/623 (我的主站) *本文将主要说明:PHP实现邮箱验证码验证功能,通过注册或登录向用户发送身份确认验证码,并通过判断输入 ...
- CSS3样式_实现字体发光效果
text-shadow 属性仅仅是用来设置文本阴影的,似乎并不能实现字体发光效果.其实不然,这正是 text-shadow 属性的精妙之处.当阴影的水平偏移量和垂直偏移量都为0时,阴影就和文本重合了. ...