生成器
惰性求值
说到"惰",我们就会想到"懒惰",其实"惰性求值"中的"惰"就是"懒惰"的意思,"求值"就是"运算"的意思,所以两者结合在一起的意思就是
仅仅在需要某一个数据的时候才会计算该数据,这个数据并不是先前就计算过,准备好的,而是临时开始计算,临时提供数据(像极了具有拖延症的人…)
这个特性可以解决一些元素个数巨大甚至无限的集合、列表占用内存过多的问题,因为数据是需要的时候,才会提供,内部保存的是一个算法,而不是大量的数据
生成器(generator)
就是惰性求值的体现,生成器中保存的是算法,节省内存空间,而且生成器实际是一种特殊的迭代器(可以通过Python——迭代器中提供的工具进行验证),由于内部保存的是算法,而通过算法是可以得到无限个数据的,这也是生成器的一大优势
- 针对于生成器的判断,提供一个途径
- 可以导入collections.abc模块中的Generator,配合函数isinstance,实现准确判断
创建生成器的途径
- 生成器的创建有两种方式
- 通过生成器生成式
- 通过在函数代码块中加入yield语句(无论该yield语句是否会被执行),再进行函数调用
生成器生成式
生成器生成式就是将列表生成式的中括号改为小括号就可以(这一改可不是啥元组生成式,元组压根没有生成式)
from collections.abc import Generator
gen = (num for num in range(10))
print(gen)
print(isinstance(gen, Generator))
#输出结果:
"""
<generator object <genexpr> at 0x0000023848003120>
True
"""
既然生成器为一种迭代器,那生成器必然就可以使用for循环进行遍历
gen = (num for num in range(10))
for num in gen:
print(num, end = ' ')
#输出结果:0 1 2 3 4 5 6 7 8 9
每一次使用生成器表达式,得到的生成器并不是同一个
gen_1 = (num for num in range(10))
gen_2 = (num for num in range(10))
print(gen_1)
print(gen_2)
#输出结果:
"""
<generator object <genexpr> at 0x0000018A31CF2120>
<generator object <genexpr> at 0x0000018A31CF2190>
"""
佐证生成器内部保存的是算法的案例
from time import time
start_time = time()
gen = (i for i in range(500000000000000000))
end_time = time()
print("程序运行所花费的时间:", end_time - start_time)
#输出结果:程序运行所花费的时间: 0.0
from time import time
start_time = time()
lst = [i for i in range(50000000)]
end_time = time()
print("程序运行所花费的时间:", end_time - start_time)
#输出结果:程序运行所花费的时间: 12.44756817817688
上面生成器运行的时间应该是近似为0
可以看到即使是生成器生成式的数据量高了列表生成式几个数量级,还是比列表生成式快得多,因为生成器保存的是算法,而列表生成式需要将所有的数据都计算出来保存到一个列表中,于是需要大量的时间去进行运算,列表保存的是全部的数据
其实生成器生成式可以实现的功能远不如后面要讲的函数形式创建的生成器多,但是就像使用列表推导式创建列表对象一样,图一个简洁、方便
函数形式创建生成器
以函数形式创建生成器,就是在函数代码块中添加yield语句后(无论该yield语句会不会被执行到),再进行函数调用,就会创建一个生成器(调用该函数的时候,并不会执行该函数代码块中的代码,而是直接返回一个生成器,然后程序就继续向下执行)
from collections.abc import Generator
def func():
while False:
yield 10
gen = func()
print(gen)
print(isinstance(gen, Generator))
#输出结果:
"""
<generator object func at 0x00000208FF062120>
True
"""
每一次调用,都会创建一个新的生成器
def func():
while False:
yield 10
gen_1 = func()
gen_2 = func()
print(gen_1)
print(gen_2)
#输出结果:
"""
<generator object func at 0x0000011667DD2120>
<generator object func at 0x0000011667DD2190>
"""
生成器的运行机制
- 如果是第一次对生成器调用next函数,则从函数的代码块的起始位置开始执行,直到遇到yield语句,停止代码执行,并将yield语句中的数据返回
- 如果不是第一次对生成器调用next函数,则从上一次暂停的位置(一个yield语句)继续执行,直到再次遇到yield语句,停止代码执行,并将yield语句中的数据返回
- 如果在对生成器调用next函数,执行代码的过程中,函数代码块执行结束,则会抛出StopIteration异常
单单看文字可能比较抽象,下面给出几个代码演示一下效果
既然生成器为迭代器对象,自然就具有__next__方法,就可以通过对其调用next函数,从而实现一次迭代操作
def func():
print('---1---')
yield 10
print('---2---')
gen = func()
print(next(gen))
#输出结果:
"""
---1---
10
"""
可以看到我们对生成器调用了一次next函数,得到的10就是yield语句中的那个10,而且当执行到yield语句的时候,就会停止函数块中的代码执行,并将数据10返回,所以我们只看到了"—1—“,而不会看到”—2—"打印出来
如果我们再调用一次呢?
def func():
print('---1---')
yield 10
print('---2---')
gen = func()
print(next(gen))
print('---3---')
print(next(gen))
#输出结果:
"""
---1---
10
---3---
---2---
"""
#打印完上面的信息以后,引发异常:没有任何异常信息,异常类型为StopIteration
通过上面的运行结果,我们可以知道第一次对生成器调用next函数,函数块中的代码运行到yield语句结束,并返回一个数据10,第二次对生成器调用next函数,是紧接着上次停下的那个yield语句继续运行函数块中的代码,于是打印出了"—2—",但是后面由于没有任何的yield语句了,就会抛出异常,而且异常类型为我们熟悉的StopIteration,我们知道,for循环就是通过捕获该异常而停止迭代操作的
函数内部是可以具有多个yield语句的
def func():
print('---1---')
yield 10
print('---2---')
yield 20
print('---3---')
yield 30
gen = func()
print(next(gen))
print('---4---')
print(next(gen))
print('---5---')
print(next(gen))
#输出结果:
"""
---1---
10
---4---
---2---
20
---5---
---3---
30
"""
运行过程为:
- 先创建一个生成器,对该生成器调用next函数,函数代码块从起始位置开始运行,先打印’—1—',接着遇到yield语句,代码停止执行,并返回数据10,打印10
- 打印’—4—’
- 对该生成器再次调用next函数,函数代码块从上次的那个yield语句继续执行,打印’—2—',再次遇到yield语句,代码停止执行,并返回数据20,打印20
- 打印’—5—’
- 对该生成器再次调用next函数,函数代码块从上次的那个yield语句继续执行,打印’—3—',再次遇到yield语句,代码停止执行,并返回数据30,打印30
- 程序运行结束
函数代码块中也可以出现return语句,不过执行return就会立即抛出StopIteration异常,return后面的信息即为异常信息(即return后面的信息会被StopIteration异常暂存起来,可以使用except StopIteration as XX将信息捕获并进行打印)
def func():
print('---1---')
return '该生成器数据提取结束'
print('---2---')
yield 10
try:
gen = func()
print(next(gen))
except StopIteration as ret:
print(ret)
#输出结果:
"""
---1---
该生成器数据提取结束
"""
从上面代码运行结果可以看出,return语句可以提前抛出异常
其实之前没有return语句的代码也可以看作是Python在函数代码块运行结束以后,会自动执行一个return语句,返回了一个None,即相对于执行了return None
def func():
print('---1---')
return None
print('---2---')
yield 10
try:
gen = func()
print(next(gen))
except StopIteration as ret:
print(ret)
#输出结果:
"""
---1---
"""
当然,该函数还是可以正常传入参数的
def func(data):
print('---1---')
yield data
gen = func(10)
print(next(gen))
#输出结果:
"""
---1---
10
"""
生成器的sent方法
- 生成器具有sent方法,通过send方法可以向生成器内部传入一个数据,改变内部算法的一些参数,使得生成器的使用更加灵活
- sent方法传入的数据在生成器内部同时必须要有变量进行接收(其实本质和函数传参没什么区别,都是传入引用)
def func():
print('---1---')
data = yield 10 #变量data负责接收send方法传入的数据
print('---2---')
print(data)
print('---3---')
yield 20
gen = func()
print(next(gen))
print('---4---')
print(gen.send(30))
#输出结果:
"""
---1---
10
---4---
---2---
30
---3---
20
"""
运行过程为:
- 先创建一个生成器,对该生成器调用next函数,函数代码块从起始位置开始运行,先打印’—1—',接着遇到yield语句,代码停止执行,并返回数据10,打印10
- 打印’—4—’
- 对该生成器调用其send方法,生成器内部接收send传入的数据30,函数代码块从上次的那个yield语句继续执行,打印’—2—‘,打印接收到的数据30,打印’—3—',再次遇到yield语句,代码停止执行,并返回数据20,打印20
- 程序运行结束
如果在第一次就调用send方法会怎么样?
def func():
data = yield 10
print(data)
gen = func()
print(gen.send(20))
#引发异常:can't send non-None value to a just-started generator
异常信息为不能在第一次调用send方法的时候,传入一个非空值的数据给生成器
其实也比较好理解,这个send方法可以理解为在传入一个数据的同时,使得函数代码块运行,这个传入的数据,必须要有变量接收,而对于一个内部算法还没执行到yield语句的生成器来说,send传入的数据是不可能被任何变量接收的,没有任何意义,于是要求必须传入的是一个空值None
def func():
data = yield 10
print(data)
gen = func()
print(gen.send(None))
#输出结果:10
如果使用的是next函数,则不会传入任何数据,仅仅就是让函数代码块继续运行
def func():
data = yield 10
print(data)
yield 20
gen = func()
print(next(gen))
print(next(gen))
#输出结果:
"""
10
None
20
"""
运行过程为:
- 先创建一个生成器,对该生成器调用next函数,函数代码块从起始位置开始运行,遇到yield语句,代码停止执行,并返回数据10,打印10
- 对该生成器再次调用next函数,由于next函数不会向生成器内部传入参数,于是data变量保存的是None的id,打印出None,函数代码块从上次的那个yield语句继续执行,,再次遇到yield语句,代码停止执行,并返回数据20,打印20
- 程序运行结束
可以看出,next函数与生成器的send方法的相同点与区别
- 相同点就是调用以后,都可以使得函数代码块运行(第一次调用)
- 不同点就是next函数不能传入数据,而send方法在调用时可以向生成器算法中传入数据,但是send方法在第一次调用的时候,传入的必须是None,否则会抛出异常
生成器与迭代器的比较
斐波那契数列数列的迭代器实现
class my_iterator:
def __init__(self):
self.num1 = 1
self.num2 = 1
def __next__(self):
data = self.num1
self.num1 = self.num2
self.num2 = data + self.num2
return data
def __iter__(self):
return self
Iterator = my_iterator()
for i in range(10):
print(next(Iterator), end = ' ')
#输出结果:1 1 2 3 5 8 13 21 34 55
斐波那契数列数列的生成器实现
def func():
num1 = 1
num2 = 1
while True:
data = num1
num1 = num2
num2 = data + num2
yield data
gen = func()
for i in range(10):
print(next(gen), end = ' ')
#输出结果:1 1 2 3 5 8 13 21 34 55
明显,迭代器的实现方式中,添加了除算法以外的其他东西,而生成器则是直击要害,仅仅写一个算法就可以实现同样的功能,所以相比之下,生成器的实现方式明显较为简便,当然,还是要看具体需求来决定要使用生成器还是迭代器
其实使用迭代器也可以验证生成器确实保存的是算法而不是数据
class my_iterable:
def __init__(self):
self.lst = list()
def add(self, data): #实现数据的添加
self.lst.append(data)
def __iter__(self): #返回一个迭代器对象(创建一个新的迭代器返回)
print('my_iterable的__iter__被调用')
return my_iterator(self.lst)
class my_iterator:
def __init__(self, lst):
print('my_iterator的__init__被调用')
self.lst = lst
self.count = 0
def __iter__(self): #返回一个迭代器对象(即返回自身)
print('my_iterator的__iter__被调用')
return self
def __next__(self):
print('my_iterator的__next__被调用')
if self.count < len(self.lst):
data = self.lst[self.count]
self.count += 1
return data
else: #如果下标超出范围,抛出StopIteration异常
raise StopIteration
Iterable = my_iterable()
Iterable.add(1)
Iterable.add(2)
Iterable.add(3)
gen = (i for i in Iterable)
print(gen)
#输出结果:
"""
my_iterable的__iter__被调用
my_iterator的__init__被调用
<generator object <genexpr> at 0x000001E2774CE200>
"""
从上面代码的运行结果可以看出,最后在运行代码"gen = (i for i in Iterable)"的时候,是调用了my_iterable类对象中的__iter__方法和my_iterator类对象中的__init__方法,取出可迭代对象的迭代器,然后并没有去调用迭代器的__next__方法,即没有取数据的操作,仅仅取出了迭代器而已