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函数,而不是像标准装饰器中的闭包

posted @ 2019-09-24 19:25  shuoliuchn  阅读(134)  评论(0编辑  收藏  举报