zoukankan      html  css  js  c++  java
  • day 13 闭包和装饰器

    day 13 闭包和装饰器

    今日内容概要

    1. 闭包
    2. 装饰器初识
    3. 标准版装饰器

    昨日内容回顾

    1. 推导式

      • 列表推导式:

        [i for i in range(3)]
        [i for i in range(9) if i % 3 == 0]
        
      • 字典推导式:

        {i: i + 1 for i in range(3)}
        {i: i + 1 for i in range(9) if i % 3 == 0}
        
      • 集合推导式:

        {i for i in range(3)}
        {i for i in range(9) if i % 3 == 0}
        
    2. 内置函数一

      all()
      any()
      callable()
      hash()
      oct()
      hex()
      divmod()
      dir()
      help()
      complex()
      id()
      print()
      input()
      repr()
      frozenset()
      eval()    # 禁用
      exec()    # 禁用
      chr()
      ord()
      round()
      bytes()
      
    3. 内置函数二

      # 高阶函数
      abs()
      sum()
      max()
      min()
      map(规则函数,可迭代对象,可迭代对象)
      filter(规则函数,可迭代对象)
      zip()
      reversed(可迭代对象)
      sorted(可迭代对象,key=规则函数)
      reduce(累加函数,可迭代对象)
      format()
      enumerate
      pow()
      
      lambda 匿名函数
      lambda 形参:返回值
      
      lst = [1,2,3,4,5,7]
      print(list(reversed(lst)))
      print(lst)
      

    今日内容详细

    闭包

    在编程时,我们会处理到很多数据。但是对于一些数据,我们只想使用,不想修改。我们可以使用闭包来防止不经意间的数据修改。

    闭包的作用主要有两个:

    1. 保护数据安全
    2. 保护数据干净性

    满足下面两个条件的函数就是一个实用的闭包:

    1. 在嵌套函数内,使用非全局变量(且不使用本层变量)
    2. 将嵌套函数本身返回

    我们可以用.__closure__方法来查看一个函数是否是一个闭包。当返回值为None时,说明该函数不是闭包,当返回值中有变量的内存地址时,说明该函数是一个闭包。

    例如,下面的函数就是一个闭包:

    def func():
        a = 10    # 自由变量
        def foo():
            print(a)
        return foo
    f = func()
    print(f.__closure__)
    
    输出的结果为:(<cell at 0x000001F7D6277648: int object at 0x00000000669D8190>,)
    

    方法中的closure就是闭包的意思。返回的cell是一个单元格对象,用来储存闭包中需要使用到的变量。

    当函数执行完成后(也就是fun()这个步骤),函数体会被销毁。但是闭包foo被返回给了f,还会被用到,所以照常存在。而闭包中需要使用的变量a,此时已经不在函数func中,因为func本身已经不复存在。变量a在函数销毁后,升级为自由变量

    闭包是怎么做到保护数据的作用的呢?

    比如我们现在有这样一个需求:使用一个函数,每天输入当天营业额,打印出这几天的平均营业额。

    如果我们把营业额的数据储存在列表中,放到全局,可以这样实现功能:

    lst = []
    def ave_turnover(turnover_today):
        lst.append(turnover_today)
        ave = sum(lst) / len(lst)
        print(ave)
    ave_turnover(12000)
    ave_turnover(15000)
    ave_turnover(14000)
    ave_turnover(12000)
    ave_turnover(25000)
    ave_turnover(21000)
    ave_turnover(9000)
    
    输出的结果为:
    12000.0
    13500.0
    13666.666666666666
    13250.0
    15600.0
    16500.0
    15428.57142857143
    

    如果有一天,我们不小心在全局对lst列表做了修改:

    lst = []
    def ave_turnover(turnover_today):
        lst.append(turnover_today)
        ave = sum(lst) / len(lst)
        print(ave)
    ave_turnover(12000)
    ave_turnover(15000)
    lst[1] = 10
    ave_turnover(14000)
    ave_turnover(12000)
    ave_turnover(25000)
    ave_turnover(21000)
    ave_turnover(9000)
    
    输出的结果为:
    12000.0
    13500.0
    8670.0
    9502.5
    12602.0
    14001.666666666666
    13287.142857142857
    

    这就导致了数据的改变,使得第三天之后的数据全都不准确。

    如果我们把重要的数据lst封装到闭包中保护起来,就不会有这种烦恼了:

    def ave_turnover():
        lst = []
        def inner(turnover_today):
            lst.append(turnover_today)
            ave = sum(lst) / len(lst)
            print(ave)
        return inner
    f = ave_turnover()
    f(12000)
    f(15000)
    lst = [10]
    f(14000)
    f(12000)
    f(25000)
    f(21000)
    f(9000)
    
    输出的结果为:
    12000.0
    13500.0
    13666.666666666666
    13250.0
    15600.0
    16500.0
    15428.57142857143
    

    对于闭包,除了.__closure__方法之外,还有可以用来查看自由变量的.__code__.co_freevars方法,和可以用来查看局部变量的.__code__.co_varnames方法:

    def ave_turnover():
        lst = []
        def inner(turnover_today):
            lst.append(turnover_today)
            ave = sum(lst) / len(lst)
            print(ave)
        return inner
    f = ave_turnover()
    print(f.__closure__)             # 判定是否是一个闭包
    print(f.__code__.co_freevars)    # 查看自由变量
    print(f.__code__.co_varnames)    # 查看局部变量
    
    输出的结果为:
    (<cell at 0x0000027A31A27648: list object at 0x0000027A31AD8C08>,)
    ('lst',)
    ('turnover_today', 'ave')
    

    没有将嵌套的函数返回也可以是一个闭包,但是这个闭包没有办法被使用:

    def func():
        a = 10
        def foo():
            print(a)
        print(foo.__closure__)
    func()
    
    输出的结果为:(<cell at 0x000001BE85EA7648: int object at 0x00000000669D8190>,)
    

    闭包也可以通过外层函数加两个括号的方法来调用:

    def func():
        a = 10
        def foo():
            print(a)
        return foo
    func()()
    

    外层函数的参数也是局部变量,所以内层函数使用外层参数的形式也可以构成闭包:

    def wrapper(a, b):
        a = 11
        b = 12
        def inner():
            print(a)
            print(b)
        return inner
    a = 11
    b = 12
    ret = wrapper(a, b)
    print(ret.__closure__)
    print(ret.__code__.co_freevars)
    print(ret.__code__.co_varnames)
    ret()
    
    返回的结果为:
    (<cell at 0x00000255942F7648: int object at 0x00000000669D81B0>, <cell at 0x00000255942F7678: int object at 0x00000000669D81D0>)
    ('a', 'b')
    ()
    11
    12
    

    闭包的应用场景主要有两个:

    1. 装饰器
    2. 防止数据被误改动

    装饰器

    在编程中,有很多约定俗成的规则。开放封闭原则就是其中很重要的一个。

    开放封闭原则体现在两个方面:

    1. 对扩展开放,支持增加新功能
    2. 对修改源代码封闭,对调用方式的改变封闭

    装饰器就是为了体现编程的开放封闭原则而存在的。

    装饰器,顾名思义,就是在原有基础上额外添加新功能的工具。

    我们有下面一组函数:

    import time
    def index():
        time.sleep(0.5)    # 休眠0.5秒,阻塞函数
        print('i am in index')
    def func():
        time.sleep(0.8)
        print('i am in func')
    def foo():
        time.sleep(0.4)
        print('i am in func')
        
    index()
    func()
    foo()
    

    现在有这样一个需求:想要知道每一个函数运行的时间。

    为了不改变函数的源代码,我们可以尝试着在调用函数前后加入查看时间戳的代码,尝试着计算函数的运行时间:

    import time
    def index():
        time.sleep(0.5)
        print('i am in index')
    def func():
        time.sleep(0.8)
        print('i am in func')
    def foo():
        time.sleep(0.4)
        print('i am in func')
        
    start_time = time.time
    index()
    print(time.time - start_time)
    
    start_time = time.time
    func()
    print(time.time - start_time)
    
    start_time = time.time
    foo()
    print(time.time - start_time)
    
    输出的结果为:
    i am in index
    0.500666618347168
    i am in func
    0.800020694732666
    i am in func
    0.4008512496948242
    
    

    我们好像得到我们想要的结果了。但是不要忘了,我们一开始提到的,我们编程时需要遵循开放封闭原则。我们的确没有改变函数的源代码,但是调用函数时,需要增加代码。我们改变了函数的调用方式。

    另一方面,我们在调用函数的过程中,使用了大量的重复代码:start_time = time.timeprint(time.time - start_time)

    为了减少重复代码,我们或许可以尝试着使用函数,比如这样:

    import time
    def index():
        time.sleep(0.5)
        print('i am in index')
    def func():
        time.sleep(0.8)
        print('i am in func')
    def foo():
        time.sleep(0.4)
        print('i am in func')
    def run_time(f):
        start_time = time.time()
        f()
        print(time.time() - start_time)
        
    ff = index
    index = run_time
    index(ff)
    
    ff = func
    func = run_time
    func(ff)
    
    ff = foo
    foo = run_time
    foo(ff)
    
    

    在调用函数时,我之所以把函数写得这么复杂,而不是简单地写成这样:

    run_time(index)
    run_time(func)
    run_time(foo)
    
    

    是为了避免改变函数的调用方式。

    可即便绞尽脑汁,函数最终的调用方式还是发生了变化——原本函数的调用是不需要参数的,增加功能后需要增加参数ff

    不过我们已经离正确的解决办法非常近了,只差一步,就可以解决我们的困难。

    这就需要结合我们今天刚刚学到的内容:闭包。

    如果我们把功能函数整合为闭包,就可以满足编程的开放封闭原则,并且不会增加太多的重复代码:

    import time
    def index():
        time.sleep(0.5)
        print('i am in index')
    def func():
        time.sleep(0.8)
        print('i am in func')
    def foo():
        time.sleep(0.4)
        print('i am in func')
    def run_time(f):
        def inner():
            start_time = time.time()
            f()
            print(time.time() - start_time)
        return inner
    index = run_time(index)
    index()
    func = run_time(func)
    func()
    foo = run_time(foo)
    foo()
    
    

    上面的这种给函数增加功能的方法就构成了Python中的装饰器。

    因为装饰器在Python编程中十分好用,Python还专门为类似index = run_time(index)的赋值运算设定了一个语法糖:

    import time
    def run_time(f):
        def inner():
            start_time = time.time()
            f()
            print(time.time() - start_time)
        return inner
    
    @run_time    # 等价于 index = run_time(index)
    def index():
        time.sleep(0.5)
        print('i am in index')
    
    index()
    
    

    语法糖必须要放在被装饰函数的正上方。虽然有些空行,Python解释器也能识别,但是阅读起来会很别扭:

    @run_time
    
    
    
    def index():
        time.sleep(0.5)
        print('i am in index')
    
    

    标准装饰器

    如果原函数中有参数,我们可以在装饰器的内部函数中设置加入参数:

    def plugin(f):
        def inner(user, pwd, hero):
            print('外挂开启')
            f(user, pwd, hero)
            print('外挂结束')
        return inner
    @plugin
    def gamming(user, pwd, hero):
        print('打开游戏')
        print(f'用户名:{user},密码:{pwd}')
        print(f'选择英雄:{hero}')
        print('游戏中')
        print('游戏结束')
    gamming('meet', '1234', '草丛伦')
    
    输出的结果为:
    外挂开启
    打开游戏
    用户名:meet,密码:1234
    选择英雄:草丛伦
    游戏中
    游戏结束
    外挂结束
    
    

    其实,在装饰器中,我们可以使用*args接收全部位置参数,使用**kwargs接收全部关键字参数。在调用函数时,只需要把args**kwargs重新打散即可:

    def plugin(f):
        def inner(*args, **kwargs):
            print('外挂开启')
            f(*args, **kwargs)
            print('外挂结束')
        return inner
    @plugin
    def gamming(user, pwd, hero):
        print('打开游戏')
        print(f'用户名:{user},密码:{pwd}')
        print(f'选择英雄:{hero}')
        print('游戏中')
        print('游戏结束')
    gamming('meet', '1234', '草丛伦')
    
    

    修饰过的gamming函数,不论是调用方法,还是参数使用,就是传参数后报错的内容都是跟修饰前的gamming一致。这就完美地符合了编程的开放封闭原则。

    除了使用参数,函数的另外一个特点是可以有返回值。如果被修饰的参数也有返回值,我们只需在装饰器的内层函数中加入返回值即可:

    def plugin(f):
        def inner(*args, **kwargs):
            print('外挂开启')
            a = f(*args, **kwargs)
            print('外挂结束')
            return a
        return inner
    @plugin
    def gamming(user, pwd, hero):
        print('打开游戏')
        print(f'用户名:{user},密码:{pwd}')
        print(f'选择英雄:{hero}')
        print('游戏中')
        print('游戏结束')
        return '我卢**没有开挂!'
    print(gamming('meet', '1234', '草丛伦'))
    
    输出的结果为:
    外挂开启
    打开游戏
    用户名:meet,密码:1234
    选择英雄:草丛伦
    游戏中
    游戏结束
    外挂结束
    我卢**没有开挂!
    
    

    至此,我们已经把装饰器的内容都讨论过了。其实,装饰器本身并不复杂,我们可以使用一个很规整简洁的代码写出一个装饰器:

    def wrapper(func):
        def inner(*args, **kwargs):
            """执行被装饰函数前,进行的操作"""
            ret = func(*args, **kwargs)
            """执行被修饰函数后,进行的操作"""
            return ret
        return inner
    @wrapper
    def foo():
        print('in foo')
    foo()
    
    

    这就是标准版的装饰器

    其实,对于没有参数的函数,可以只写一层函数实现装饰器的功能:

    def foo(func):
        print('新加了一个功能')
        return func
    @func
    def index():
        print(2)
    index()
    
    输出的结果为:
    新加了一个功能
    2
    
    

    虽然也能实现新功能,但是不建议这样写,因为不是很规范。而且下面继续调用index,并不会有新功能加入:

    def foo(func):
        print('新加了一个功能')
        return func
    @func    # index = func(index) --> index = index
    def index():
        print(2)
    index()
    index()
    
    输出的结果为:
    新加了一个功能
    2
    2
    
    

    函数调用两次,但是新加功能仅执行了一次。这是因为在执行语法糖时,新的index最终的结果还是原来的index函数,而不是像标准装饰器中的闭包

  • 相关阅读:
    python 生成器 迭代器
    廖---高级特性 切片 迭代 列表生成式
    汉诺塔
    廖---函数
    廖---控制流
    廖---list tuple dic set
    廖---字符串和编码
    MySQL常见的三种存储引擎
    mysql悲观锁以及乐观锁总结和实践
    数据库事务的四大特性以及事务的隔离级别
  • 原文地址:https://www.cnblogs.com/shuoliuchina/p/11580388.html
Copyright © 2011-2022 走看看