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()

 

  严格来来说装饰器只是语法糖。

  装饰的特性

  1.  能把被装饰的函数转成其他函数
  2. 装饰器在加载模块时立即执行(这一点在后续说明(以及加载在装饰函数和内函数之间的部分如何执行)) 
       装饰器的一个关键特性是:他们在被装饰的函数定义之后立即运行。(这通常在导入时())



 

  装饰器何时执行

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()

 

 

  1.  registry 保存被@register装饰的函数引用
  2. register的参数是一个函数
  3. 为了演示显示被装饰的函数
  4. 把func存入registry
  5. 返回func:必须返回函数,这里返回的函数与通过参数传入的一样。
  6. f1和f2被@refister装饰
  7. f3没有装饰
  8. main显示registry,然后调用f1(),f2()和f3()
  9. 只有把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().

 

 

实现一个简单的装饰器

了解了装饰器以及装饰器的概念,下面看一个实现装饰器的例子;

实现一个输出函数运行时间的装饰器

 装饰器代码:单独存在一个模块中

    

 

 

  注释:

  1.   第10行,定义内部函数clocked,它接受任意个定位参数
  2.        第12行,这行代码可用,是因为clocked的闭包中包含自由变量func
  3.        第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》

posted @ 2021-08-23 16:19  甲壳虫~~~  阅读(68)  评论(0)    收藏  举报