闭包

一.闭包

由于闭包这个概念比较难以理解,尤其是初学者来说,相对难以掌握,所以我们通过示例去理解学习闭包。

给大家提个需求,然后用函数去实现:完成一个计算不断增加的系列值的平均值的需求。

例如:整个历史中的某个商品的平均收盘价。什么叫平局收盘价呢?就是从这个商品一出现开始,每天记录当天价格,然后计算他的平均值:平均值要考虑直至目前为止所有的价格。

比如大众推出了一款新车:小白轿车。

第一天价格为:100000元,平均收盘价:100000元

第二天价格为:110000元,平均收盘价:(100000 + 110000)/2 元

第三天价格为:120000元,平均收盘价:(100000 + 110000 + 120000)/3 元

........

series = []
def make_averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total / len(series)

print(make_averager(100000))
print(make_averager(110000))
print(make_averager(120000))

从上面的例子可以看出,基本上完成了我们的要求,但是这个代码相对来说是不安全的,因为你的这个series列表是一个全局变量,只要是全局作用域的任何地方,都可能对这个列表进行改变。

series = []
def make_averager(new_value):
    series.append(new_value)
    total = sum(series)
    return total / len(series)

print(make_averager(100000))
print(make_averager(110000))
series.append(666)  # 如果对数据进行相应改变,那么你的平均收盘价就会出现很大的问题。
print(make_averager(120000))

那么怎么办呢?有人说,你把他放在函数中不就行了,这样不就是局部变量了么?数据不就相对安全了么?

def make_averager(new_value):
    series = []
    series.append(new_value)
    total = sum(series)
    return total / len(series)


print(make_averager(100000))  # 100000.0
print(make_averager(110000))  # 110000.0
print(make_averager(120000))  # 120000.0

这样计算的结果是不正确的,那是因为执行函数,会开启一个临时的名称空间,随着函数的结束而消失,所以你每次执行函数的时候,都是重新创建这个列表,那么这怎么做呢?这种情况下,就需要用到我们讲的闭包了,我们用闭包的思想改一下这个代码。

def make_averager():

    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager

avg = make_averager()
print(avg(100000))
print(avg(110000))
print(avg(120000))

大家仔细看一下这个代码,我是在函数中嵌套了一个函数。那么avg 这个变量接收的实际是averager函数名,也就是其对应的内存地址,我执行了三次avg 也就是执行了三次averager这个函数。那么此时你们有什么问题?

肯定有学生就会问,那么我的make_averager这个函数只是执行了一次,为什么series这个列表没有消失?反而还可以被调用三次呢?这个就是最关键的地方,也是闭包的精华所在。我给大家说一下这个原理,以图为证:

img

上面被红色方框框起来的区域就是闭包,被蓝色圈起来的那个变量应该是make_averager()函数的局部变量,它应该是随着make_averager()函数的执行结束之后而消失。但是他没有,是因为此区域形成了闭包,series变量就变成了一个叫自由变量的东西,averager函数的作用域会延伸到包含自由变量series的绑定。也就是说,每次我调用avg对应的averager函数 时,都可以引用到这个自用变量series,这个就是闭包。

闭包的定义:

  1. 闭包是嵌套在函数中的函数。
  2. 闭包必须是内层函数对外层函数的变量(非全局变量)的引用。

如何判断判断闭包?举例让同学回答:

# 例一:
def wrapper():
    a = 1
    def inner():
        print(a)
    return inner
ret = wrapper()

# 例二:
a = 2
def wrapper():
    def inner():
        print(a)
    return inner
ret = wrapper()


# 例三:

def wrapper(a,b):
    def inner():
        print(a)
        print(b)
    return inner
a = 2
b = 3
ret = wrapper(a,b)

以上三个例子,最难判断的是第三个,其实第三个也是闭包,如果我们每次去研究代码判断其是不是闭包,有一些不科学,或者过于麻烦了,那么有一些函数的属性是可以获取到此函数是否拥有自由变量的,如果此函数拥有自由变量,那么就可以侧面证明其是否是闭包函数了(了解):

def make_averager():

    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager
avg = make_averager()
# 函数名.__code__.co_freevars 查看函数的自由变量
print(avg.__code__.co_freevars)  # ('series',)
当然还有一些参数,仅供了解:

# 函数名.__code__.co_freevars 查看函数的自由变量
print(avg.__code__.co_freevars)  # ('series',)
# 函数名.__code__.co_varnames 查看函数的局部变量
print(avg.__code__.co_varnames)  # ('new_value', 'total')
# 函数名.__closure__ 获取具体的自由变量对象,也就是cell对象。
# (<cell at 0x0000020070CB7618: int object at 0x000000005CA08090>,)
# cell_contents 自由变量具体的值
print(avg.__closure__[0].cell_contents)  # []

闭包的作用:保存局部信息不被销毁,保证数据的安全性。

闭包的应用

  1. 可以保存一些非全局变量但是不易被销毁、改变的数据。
  2. 装饰器。

二. 推导式

本节我们讲列表推导式,生成器表达式以及其他推导式,我认为推导式就是构建比较有规律的列表,生成器,字典等一种简便的方式。那么他如何简便呢?看下面的例题:

2.1列表推导式

这里让学生自己做一下,首先我们先看一下这样的代码,给出一个列表,通过循环,想列表中添加1~10:

li = []

for i in range(10):

    li.append(i)

print(li)

那么按照上面的要求我们用列表推导式写一下:

ls = [i for i in range(10)]

print(ls)

怎么样?一行搞定,上面这个代码就是列表推导式,接下来我们将列表推导式进行一个分类:

列表推导式分为两种模式:

1.循环模式:[变量(加工的变量) for 变量 in iterable]

2.筛选模式: [变量(加工的变量) for 变量 in iterable if 条件]

当然还有多层循环的,这个我们一会就会讲到,那么我们先来看循环模式。

2.1.1 循环模式

刚才我们看到的就是循环模式,那么有同学会问到,什么叫' 加工的变量'? 这个也比较简单,接下来我们做几道题:

  1. 将10以内所有整数的平方写入列表。
l1 = [i*i for i in range(1,11)]
print(l1)

  1. 100以内所有的偶数写入列表.
l1 = [i for i in range(2,101,2)]
print(l1)

  1. 从python1期到python24期写入列表lst
lst = [f'python{i}' % i for i in range(1,25)]

print(lst)

上面那个格式化输出的变量f'python{i}',就是加工的变量。

上面做的那三个就是循环模式,比较简单,接下来我们研究筛选模式。

2.1.2 筛选模式

筛选模式就是在上面的基础上加上一个判断条件,将满足条件的变量留到列表中。

带着同学们做一个题:

将这个列表中大于3的元素留下来。

l1 = [4, 3, 2, 6, 5, 5, 7, 8] 
print([i for i in l1 if i > 3])

通过我给大家的演示,大家做几道题:

  1. 三十以内可以被三整除的数。

     multiples = [i for i in range(30) if i % 3 is 0]
     print(multiples)
    
    
  2. 过滤掉长度小于3的字符串列表,并将剩下的转换成大写字母

     l = ['wusir', 'laonanhai', 'aa', 'b', 'taibai']
     # print([i.upper() for i in l if len(i) > 3])
    
    
  3. 找到嵌套列表中名字含有两个‘e’的所有名字(有难度

     names = [['Tom', 'Billy', 'Jefferson', 'Andrew', 'Wesley', 'Steven', 'Joe'],
              ['Alice', 'Jill', 'Ana', 'Wendy', 'Jennifer', 'Sherry', 'Eva']]
    
     print([name for lst in names for name in lst if name.count('e') >= 2])  
     # 注意遍历顺序,这是实现的关键
    
    

列表推导式基本上讲完了,当然今天会做一些有关列表推导式的题,让大家更加深入的了解。

2.1.3 生成器表达式

生成器表达式和列表推导式的语法上一模一样,只是把[]换成()就行了。比如将十以内所有数的平方放到一个生成器表达式中

gen = (i**2 for i in range(10))
print(gen)
# 结果: <generator object <genexpr> at 0x0000026046CAEBF8>

生成器表达式也可以进行筛选

# 获取1-100内能被3整除的数
gen = (i for i in range(1,100) if i % 3 == 0)
for num in gen:
    print(num)

生成器表达式和列表推导式的区别:

  1. 列表推导式比较耗内存,所有数据一次性加载到内存。而生成器表达式遵循迭代器协议,逐个产生元素。
  2. 得到的值不一样,列表推导式得到的是一个列表.生成器表达式获取的是一个生成器
  3. 列表推导式一目了然,生成器表达式只是一个内存地址。

无论是生成器表达式,还是列表推导式,他只是Python给你提供了一个相对简单的构造方式,因为使用推导式非常简单,所以大多数都会为之着迷,这个一定要慎重,推导式只能构建相对复杂的并且有规律的对象,对于没有什么规律,而且嵌套层数比较多(for循环超过三层)这样就不建议大家用推导式构建。

生成器的惰性机制: 生成器只有在访问的时候才取值,说白了.你找他要才给你值.不找他要.他是不会执行的.

2.1.4 其他相关的推导式(了解)

字典推导式

根据名字应该也能猜到,推到出来的是字典

lst1 = ['jay','jj','meet']
lst2 = ['周杰伦','林俊杰','郭宝元']
dic = {lst1[i]:lst2[i] for i in range(len(lst1))}
print(dic)

集合推导式

集合推导式可以帮我们直接生成一个集合,集合的特点;无序,不重复 所以集合推导式自带去重功能

lst = [1,2,3,-1,-3,-7,9]
s = {abs(i) for i in lst}
print(s)

三 .迭代器

3.1 可迭代对象

  3.1.1 可迭代对象定义

对于迭代器来说,我们更熟悉的应该是可迭代对象,之前无论是源码还是讲课中或多或少我们提到过可迭代对象这个词。之前为了便于大家理解可迭代对象,可能解释的不是很正确,所以今天我们正式的聊一聊什么是可迭代对象。从字面意思来说,我们先对其进行拆解:什么是对象?Python中一切皆对象,之前我们讲过的一个变量,一个列表,一个字符串,文件句柄,函数名等等都可称作一个对象,其实一个对象就是一个实例,就是一个实实在在的东西。那么什么叫迭代?其实我们在日常生活中经常遇到迭代这个词儿,更新迭代等等,迭代就是一个重复的过程,但是不能是单纯的重复(如果只是单纯的重复那么他与循环没有什么区别)每次重复都是基于上一次的结果而来。比如你爹生你,你生你爹,哦不对,你生你儿子,你儿子生你孙子等等,每一代都是不一样的;还有你使用过得app,微信,抖音等,隔一段时间就会基于上一次做一些更新,那么这就是迭代。可迭代对象从字面意思来说就是一个可以重复取值的实实在在的东西。

那么刚才我们是从字面意思分析的什么是可迭代对象,到目前为止我们接触到的可迭代对象有哪些呢?

str list tuple dic set range 文件句柄等,那么int,bool这些为什么不能称为可迭代对象呢?虽然在字面意思这些看着不符合,但是我们要有一定的判断标准或者规则去判断该对象是不是可迭代对象。

在python中,但凡内部含有*iter*方法的对象,都是可迭代对象

  3.1.2 查看对象内部方法

该对象内部含有什么方法除了看源码还有什么其他的解决方式么?当然有了, 可以通过dir() 去判断一个对象具有什么方法

s1 = 'alex'
print(dir(s1))

dir()会返回一个列表,这个列表中含有该对象的以字符串的形式所有方法名。这样我们就可以判断python中的一个对象是不是可迭代对象了:

s1 = 'alex'
i = 100
print('__iter__' in dir(i))  # False
print('__iter__' in dir(s1))  # True

  3.1.3 小结

从字面意思来说:可迭代对象就是一个可以重复取值的实实在在的东西。

从专业角度来说:但凡内部含有__iter__方法的对象,都是可迭代对象。

可迭代对象可以通过判断该对象是否有__iter__方法来判断。

可迭代对象的优点:

可以直观的查看里面的数据。

可迭代对象的缺点:

1.占用内存。

2.可迭代对象不能迭代取值(除去索引,key以外)。

那么这个缺点有人就提出质疑了,即使抛去索引,key以外,这些我可以通过for循环进行取值呀!对,他们都可以通过for循环进行取值,其实for循环在底层做了一个小小的转化,就是先将可迭代对象转化成迭代器,然后在进行取值的。那么接下来,我们就看看迭代器是个什么鬼。

3.2 迭代器

  3.2.1 迭代器的定义

从字面意思来说迭代器,是一个可以迭代取值的工具,器:在这里当做工具比较合适。

从专业角度来说:迭代器是这样的对象:实现了无参数的__next__方法,返回序列中的下一个元素,如果没有元素了,那么抛出StopIteration异常.python中的迭代器还实现了__iter__方法,因此迭代器也可以迭代。 出自《流畅的python》

那么对于上面的解释有一些超前,和难以理解,不用过于纠结,我们简单来说:在python中,内部含有__Iter__方法并且含有__next__方法的对象就是迭代器。

  3.2.2 如何判断该对象是否是迭代器

ok,那么我们有了这个定义,我们就可以判断一些对象是不是迭代器或者可迭代对象了了,请判断这些对象:str list tuple dict set range 文件句柄 哪个是迭代器,哪个是可迭代对象:

o1 = 'alex'
o2 = [1, 2, 3]
o3 = (1, 2, 3)
o4 = {'name': '宝元','age': 18}
o5 = {1, 2, 3}
f = open('file',encoding='utf-8', mode='w')
print('__iter__' in dir(o1))  # True
print('__iter__' in dir(o2))  # True
print('__iter__' in dir(o3))  # True
print('__iter__' in dir(o4))  # True
print('__iter__' in dir(o5))  # True
print('__iter__' in dir(f))  # True

print('__next__' in dir(o1))  # False
print('__next__' in dir(o2))  # False
print('__next__' in dir(o3))  # False
print('__next__' in dir(o4))  # False
print('__next__' in dir(o5))  # False
print('__next__' in dir(f))  # True
f.close()

通过以上代码可以验证,之前我们学过的这些对象,只有文件句柄是迭代器,剩下的那些数据类型都是可迭代对象。

  3.2.3 可迭代对象如何转化成迭代器:

l1 = [1, 2, 3, 4, 5, 6]
obj = l1.__iter__()
# <list_iterator object at 0x000002057FE1A3C8>
# 或
obj = iter(l1)
print(obj)
# <list_iterator object at 0x102cc67f0>

 3.2.4 迭代器取值:

可迭代对象是不可以一直迭代取值的(除去用索引,切片以及Key),但是转化成迭代器就可以了,迭代器是利用__next__()进行取值:

l1 = [1, 2, 3,]
obj = l1.__iter__()  # 或者 iter(l1)
# print(obj)  # <list_iterator object at 0x000002057FE1A3C8>
ret = obj.__next__()
print(ret)
ret = obj.__next__()
print(ret)
ret = obj.__next__()
print(ret)
ret = obj.__next__()  # StopIteration
print(ret)
# 迭代器利用next取值:一个next取对应的一个值,如果迭代器里面的值取完了,还要next,
# 那么就报StopIteration的错误。

  3.2.5 while模拟for的内部循环机制:

刚才我们提到了,for循环的循环对象一定要是可迭代对象,但是这不意味着可迭代对象就可以取值,因为for循环的内部机制是:将可迭代对象转换成迭代器,然后利用next进行取值,最后利用异常处理处理StopIteration抛出的异常。

l1 = [1, 2, 3, 4, 5, 6]
# 1 将可迭代对象转化成迭代器
obj = iter(l1)
# 2,利用while循环,next进行取值
while 1:
    # 3,利用异常处理终止循环
    try:
        print(next(obj))
    except StopIteration:
        break

  3.2.6 小结:

从字面意思来说:迭代器就是可以迭代取值的工具。

从专业角度来说:在python中,内部含有__Iter__方法并且含有__next__方法的对象就是迭代器。

迭代器的优点:

节省内存。 迭代器在内存中相当于只占一个数据的空间:因为每次取值都上一条数据会在内存释放,加载当前的此条数据。

惰性机制。 next一次,取一个值,绝不过多取值。

有一个迭代器模式可以很好的解释上面这两条:迭代是数据处理的基石。扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式。

迭代器的缺点:

不能直观的查看里面的数据。

取值时不走回头路,只能一直向下取值。

l1 = [1, 2, 3, 4, 5, 6]
obj = iter(l1)

for i in range(2):
    print(next(obj))

for i in range(2):
    print(next(obj))

3.3 可迭代对象与迭代器对比

我们今天比较深入的了解了可迭代对象与迭代器,接下来我们说一下这两者之间比较与应用:

可迭代对象:

是一个私有的方法比较多,操作灵活(比如列表,字典的增删改查,字符串的常用操作方法等),比较直观,但是占用内存,而且不能直接通过循环迭代取值的这么一个数据集。

应用:当你侧重于对于数据可以灵活处理,并且内存空间足够,将数据集设置为可迭代对象是明确的选择。

迭代器:

是一个非常节省内存,可以记录取值位置,可以直接通过循环+next方法取值,但是不直观,操作方法比较单一的数据集。

应用:当你的数据量过大,大到足以撑爆你的内存或者你以节省内存为首选因素时,将数据集设置为迭代器是一个不错的选择。(可参考为什么python把文件句柄设置成迭代器)。

四.生成器

4.1 初识生成器

什么是生成器?这个概念比较模糊,各种文献都有不同的理解,但是核心基本相同。生成器的本质就是迭代器,在python社区中,大多数时候都把迭代器和生成器是做同一个概念。不是相同么?为什么还要创建生成器?生成器和迭代器也有不同,唯一的不同就是:迭代器都是Python给你提供的已经写好的工具或者通过数据转化得来的,(比如文件句柄,iter([1,2,3])。生成器是需要我们自己用python代码构建的工具。最大的区别也就如此了。

4.2 生成器的构建方式

在python中有三种方式来创建生成器:

  1. 通过生成器函数
  2. 通过生成器推导式
  3. python内置函数或者模块提供(其实1,3两种本质上差不多,都是通过函数的形式生成,只不过1是自己写的生成器函数,3是python提供的生成器函数而已)

4.3 生成器函数

我们先来研究通过生成器函数构建生成器。

首先,我们先看一个很简单的函数:

def func():
    print(11)
    return 22
ret = func()
print(ret)
# 运行结果:
11
22

将函数中的return换成yield,这样func就不是函数了,而是一个生成器函数

def func():
    print(11)
    yield 22

我们这样写没有任何的变化,这是为什么呢? 我们来看看函数名加括号获取到的是什么?

def func():
    print(11)
    yield 22
ret = func()
print(ret)

# 运行结果:
<generator object func at 0x000001A575163888>

为什么在函数中添加了yield在调用函数的时候就发现结果不是我们预想的结果呢,是因为当我们调用函数的时候函数体里的代码会进行执行当执行到yield的关键字的时候,发现我们是想声明一个生成器.程序就会返回一个生成器给咱们

那么生成器对象如何取值呢?

之前我们说了,生成器的本质就是迭代器.迭代器如何取值,生成器就如何取值。所以我们可以直接执行next()来执行以下生成器

def func():
     print("111")
     yield 222
gener = func() # 这个时候函数不会执⾏. ⽽是获取到⽣成器
ret = gener.__next__() # 这个时候函数才会执⾏
print(ret)  # 并且yield会将func生产出来的数据 222 给了 ret。  

结果:
111
222

并且我的生成器函数中可以写多个yield。

def func():

    print("111")

    yield 222

    print("333")

    yield 444

gener = func()

ret = gener.__next__()

print(ret)

ret2 = gener.__next__()

print(ret2)

ret3 = gener.__next__()

# 最后⼀个yield执⾏完毕. 再次__next__()程序报错
print(ret3)

当程序运行完最后一个yield,那么后面继续运行next()程序会报错,一个yield对应一个next,next超过yield数量,就会报错,与迭代器一样。

yield与return的区别:

return一般在函数中只设置一个,他的作用是终止函数,并且给函数的执行者返回值。

yield在生成器函数中可设置多个,他并不会终止函数,next会获取对应yield生成的元素。

举例:

我们来看一下这个需求:老男孩向楼下卖包子的老板订购了10000个包子.包子铺老板非常实在,一下就全部都做出来了 

def eat():

    lst = []

    for i in range(1,10000):

        lst.append('包子'+str(i))

    return lst

e = eat()

print(e)

这样做没有问题,但是我们由于学生没有那么多,只吃了2000个左右,剩下的8000个,就只能占着一定的空间,放在一边了。如果包子铺老板效率够高,我吃一个包子,你做一个包子,那么这就不会占用太多空间存储了,完美。

def eat():

    for i in range(1,10000):

        yield '包子'+str(i)

e = eat()

for i in range(200):
    next(e)

这两者的区别:

第一种是直接把包子全部做出来,占用内存。

第二种是吃一个生产一个,非常的节省内存,而且还可以保留上次的位置。

def eat():

    for i in range(1,10000):

        yield '包子'+str(i)

e = eat()

for i in range(200):
    next(e)

for i in range(300):
    next(e)
# 多次next包子的号码是按照顺序记录的。

4.4 send 方法(了解,不讲)

接下来我们再来认识一个新的东西,send方法

# next只能获取yield生成的值,但是不能传递值。
def gen(name):
    print(f'{name} ready to eat')
    while 1:
        food = yield
        print(f'{name} start to eat {food}')

dog = gen('alex')
next(dog)
next(dog)
next(dog)


# 而使用send这个方法是可以的。
def gen(name):
    print(f'{name} ready to eat')
    while 1:
        food = yield 222
        print(f'{name} start to eat {food}')

dog = gen('alex')
next(dog)  # 第一次必须用next让指针停留在第一个yield后面
# 与next一样,可以获取到yield的值
ret = dog.send('骨头')
print(ret)


def gen(name):
    print(f'{name} ready to eat')
    while 1:
        food = yield
        print(f'{name} start to eat {food}')

dog = gen('alex')
next(dog)
# 还可以给上一个yield发送值
dog.send('骨头')
dog.send('狗粮')
dog.send('香肠')

send和next()区别:

相同点:

send 和 next()都可以让生成器对应的yield向下执行一次。

都可以获取到yield生成的值。

不同点:

第一次获取yield值只能用next不能用send(可以用send(None))。

send可以给上一个yield置传递值。

4.5 yield from

在python3中提供一种可以直接把可迭代对象中的每一个数据作为生成器的结果进行返回

# 对比yield 与 yield from 
def func():
    lst = ['卫龙','老冰棍','北冰洋','牛羊配']
    yield lst
g = func()
print(g)
print(next(g))  # 只是返回一个列表

def func():
    lst = ['卫龙','老冰棍','北冰洋','牛羊配']
    yield from lst
g = func()
print(g)
# 他会将这个可迭代对象(列表)的每个元素当成迭代器的每个结果进行返回。
print(next(g))
print(next(g))
print(next(g))
print(next(g))
'''
yield from ['卫龙','老冰棍','北冰洋','牛羊配'] 
等同于:
    yield '卫龙'
    yield '老冰棍'
    yield '北冰洋'
    yield '牛羊配'

4.6 yield from 小坑

def func():
    lst1 = ['卫龙', '老冰棍', '北冰洋', '牛羊配']
    lst2 = ['馒头', '花卷', '豆包', '大饼']
    yield from lst1
    yield from lst2


g = func()
for i in g:
    print(i)

返回的结果是将第一个列表的元素全部返回后,在返回第二个列表

python
原文地址:https://www.cnblogs.com/bky20061005/p/12145006.html