python 装饰器
Python装饰器基础知识
装饰器
前提知识(深入理解-->变量的作用域范围,自由变量,闭包)
装饰器:装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
e.g.1:装饰器把函数(被装饰函数)替换成另一个函数
# -*- ecoding: utf-8 -*- # @ModuleName: demo1.py # @Function: # @Author: Zhangjie # @Time: 2021/8/23 10:06 def deco(func): def inner(): print('running inner()') return inner #1 @deco def target(): #2 print('running target()') if __name__ == '__main__': target() #3 #output running inner() print(target) #4 #output <function deco.<locals>.inner at 0x03A83850>
1 deco返回inner函数对象
2 使用deco装饰target
3 调用被装饰的target其实会运行inner
4 审查对象,发现target现在是inner的引用
解析:

上面的写法和如下代码写法是一样的

两种写法最终结果一样:上述代码片段执行完成后得到的target不是原来的那个target函数,而是deco(target)返回的那个函数
所以执行target() 时实质上执行的是inner()
严格来来说装饰器只是语法糖。
装饰的特性
- 能把被装饰的函数转成其他函数
- 装饰器在加载模块时立即执行(这一点在后续说明(以及加载在装饰函数和内函数之间的部分如何执行))
装饰器的一个关键特性是:他们在被装饰的函数定义之后立即运行。(这通常在导入时())
装饰器何时执行
e.g.2:demon2.py
# -*- ecoding: utf-8 -*- # @ModuleName: demo2 # @Function: # @Author: Zhangjie # @Time: 2021/8/23 10:38 registry = [] #1 def register(func): #2 print('running register(%s)' % func) #3 registry.append(func) #4 return func #5 @register #6 def f1(): print('running f1()') @register def f2(): print('running f2()') def f3(): #7 print('running f3()') def main(): #8 print("running main()") print("registry ->", registry) f1() f2() f3() if __name__ == '__main__': #9 main()
- registry 保存被@register装饰的函数引用
- register的参数是一个函数
- 为了演示显示被装饰的函数
- 把func存入registry
- 返回func:必须返回函数,这里返回的函数与通过参数传入的一样。
- f1和f2被@refister装饰
- f3没有装饰
- main显示registry,然后调用f1(),f2()和f3()
- 只有把demon2.py当作脚本运行时才调用main()
看看你认为的运行结果和实际是否一致:
running register(<function f1 at 0x03DB3928>) running register(<function f2 at 0x03DB38E0>) running main() registry -> [<function f1 at 0x03DB3928>, <function f2 at 0x03DB38E0>] running f1() running f2() running f3()
register 在模块中其他函数之前运行(两次)。调用register 时,传给它的参数是被装饰的函数,例如 <function f1 at 0x03DB3928>。
加载模块后,registry 中有两个被装饰函数的引用:f1 和 f2。这两个函数,以及 f3,只在 main 明确调用它们时才执行。
e.g.3:导入模块

想要说明函数装饰器再倒入模块是立即执行,而被装饰的函数只在明确调用时运行。这突出了Python程序员所说的导入时和运行时之间的区别。
- 实际情况是装饰器通常在一个模块中定义,然后应用到其他模块中的函数上(python函数式变成思想),示例中以便演示。
- register装饰器返回的函数与传入的参数相同。实际情况中大多数装饰器会在内部定义一个函数,然后将其返回。
装饰器的执行顺序
e.g.4:demo4.py
1 def dec1(func): 2 print("1111") 3 4 def one(): 5 print("2222") 6 func() 7 print("3333") 8 9 return one 10 11 12 def dec2(func): 13 print("aaaa") 14 15 def two(): 16 print("bbbb") 17 func() 18 print("cccc") 19 20 return two 21 22 23 @dec1 24 @dec2 25 def test(): 26 print("test test")
27
28 test()
比对下脑中执行的结果和实际是否一致:
实际执行结果:
aaaa 1111 2222 bbbb test test cccc 3333
解析:
test=dect1(dect2(test)),此时先执行dect2(test),结果是输出aaaa、将func指向函数test、并返回函数two,然后执行dect1(two),结果是输出1111、将func指向函数two、并返回函数one,
然后进行赋值,用函数替代了函数名test. test()实际调用被装载的函数这时实际上执行的是函数one,运行到func()时执行函数two,再运行到func()时执行未修饰的函数test。
这部分如果还没有看懂的话建议再次看下装饰器中的解析部分理解, 再理解此例中的装饰器的语法糖和普通写法相同的效果:test = dect1(dect2(test)) ,执行test()(加上括号函数调用),
实质上是就会一次赋值成装饰器返回函数的调用one() ,two().
实现一个简单的装饰器
了解了装饰器以及装饰器的概念,下面看一个实现装饰器的例子;
实现一个输出函数运行时间的装饰器
装饰器代码:单独存在一个模块中

注释:
- 第10行,定义内部函数clocked,它接受任意个定位参数
- 第12行,这行代码可用,是因为clocked的闭包中包含自由变量func
- 第18行,返回内部函数,取代被装饰的函数 下面的例子显示了clock装饰器的用法
被装饰函数:

运行结果如下:
**************************************** Calling snooze(.123) [0.13464860s] snooze(0.123) -> None **************************************** Calling factorial(6) [0.00000060s] factorial(1) -> 1 [0.00001030s] factorial(2) -> 2 [0.00001730s] factorial(3) -> 6 [0.00002360s] factorial(4) -> 24 [0.00003000s] factorial(5) -> 120 [0.00004210s] factorial(6) -> 720 6! = 720
对于被装饰函数factorial的解释:
还记得装饰器语法糖和普通写法的转换吗?
这里再加强一下理解:

在两个示例中,factorial会最为func参数传给clock.然后clock函数返回clocked函数,Python解释器在背后会把clocked赋值给factorial,
其实导入ckcodeco_demo模块后查看factorial的__name__属性,会得到如下结果:

因此factorial保存的是clockede函数的引用。
自此之后,每次调用factorial(n),执行的都是clocked(n),clocked大致做了下面几件事:
-
- 记录初始时间t0
- 调用原来的factorial函数,保存结果
- 计算结果的时间
- 格式化手机的数据并打印出来
- 返回第2步中保存的结果-->result
-
这是装饰器的典型行为:把装饰器的函数替换成新函数,二者接受相同的参数,而且(通常)返回被装饰的函数本该返回的值,同时还做了些额外的操作。
是否有这样的疑问:被装饰的factorial会输出多条记录,
而单独执行factorial()函数不加装饰器的时候就只能返回一条数据?
优化
“实现一个简单的装饰器” 在这部分计算函数运行时间装饰器的例子中有以下几个缺点:
不支持关键字参数,而且覆盖了被装饰函数的__name__和__doc__属性。
使用 functools.wraps 装饰器把相关的属性从func复制到clocked中。
此外这个还能正确处理关键字参数
改进后的代码:

导入后调用:

执行结果:
[0.13203890s] snooze(0.123) -> None **************************************** Calling factorial(6) [0.00000120s] factorial(1) -> 1 [0.00001660s] factorial(2) -> 2 [0.00002750s] factorial(3) -> 6 [0.00003780s] factorial(4) -> 24 [0.00004760s] factorial(5) -> 120 [0.00006060s] factorial(6) -> 720 6! = 720 factorial
这里实现了可变定参,和关键字参数的传入,以及函数名保持原函数名(这里的函数名称属性值不再是clocked)
参考
《流畅的python》

浙公网安备 33010602011771号