引言
倘若需要输出一组0到9的平方的数据,容易想到以下实现方法:
# 方法1(for循环)
for i in range(10):
print(i**2)
# 方法2(lamda匿名函数)
f = map((lambda x : x**2), range(10))
for i in f:
print(i)
# 方法3(列表生成式)
f = [x**2 for x in range(10)]
for i in f:
print(i)
以上方法可得到0至9的平方,但若要生成0到100000的(甚至更大的数)平方,以上方法就会出现内存占用过大的问题,此外,有时我们用列表储存一个很大的数组,但使用过程中我们只用到了最前面的几个数,就出现资源浪费的问题。因此,如果列表中的元素可以由某种算法推算出来,在循环的过程中依据该算法不断推算后续的元素,那么可以节约大量的内存。
为了解决该问题,生成器(generator)的概念被提出,即一边循环一边计算的机制。换句话说,如果以更小的内存获得更大的数据,那就用生成器。
生成器
生成器的实现方法有两种,一种是将列表生成式的[]
换成()
,另一种是在函数中使用yield
,那么在调用该函数的时候,就创建了一个生成器对象。以下是两种实现方法:
# 方法1
generate = (x**2 for x in range(10))
print(generate)
# <generator object <genexpr> at 0x0000024518D04DB0>
# 方法2
def generate(n):
i = 0
while i < n:
yield i**2
print(i**2)
i += 1
return 0
f = generate(10)
print(f)
# <generator object generate at 0x0000024518D04DB0>
以上两种方法分别创建了两个生成器对象,与列表储存数组不同,我们可以认为生成器储存的是算法,那么它能够顺利地推算出后续的数据吗,我们可以继续实验。如何输出数据呢?除了可以利用for循环输出结果,也可以利用生成器的next方法。如下:
generate = (x**2 for x in range(3))
# 方法1
for i in generate:
print(i)
# 0
# 1
# 4
generate = (x**2 for x in range(3))
# 方法2
print(next(generate))
# 0
print(generate.__next__())
# 1
print(next(generate))
# 4
print(next(generate))
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# StopIteration
可以发现,生成器确实能够顺利地推算数据。值得注意的是,输出最后一个数据后,使用next方法时,会返回一个StopIteration的异常。事实上,生成器能够迭代的原因也是next方法,重复调用该方法,直到捕捉到StopIteration异常。一般情况下,获取生成器数据不会使用next方法,而是使用for循环迭代。
生成器工作原理
总的来说,生成器可以被用作控制循环的迭代行为,每次调用yield会暂停,使用next方法时恢复生成器。因此,不同于一般函数一次性返回所有数据,生成器一次只返回一个结果,是一种惰性计算。
此外,还有send方法,它同next方法一样,可以恢复生成器;但与next不同的是,它可以传入一个新的数据作为yield表达式的整体结果。
以下具体介绍yield和send方法。
yield
首先,说明yield。yield类似 return ,它会返回一个值,并且记住这个返回的位置,下次迭代或者用next(send)恢复生成器后,代码从返回位置的下一条语句继续执行。
def generate(n):
i = 0
while i < n:
a = yield i**2
print(a)
print(i**2)
i += 1
f = generate(3)
f.__next__()
# 0
f.__next__()
# None
# 0
# 1
f.__next__()
# None
# 1
# 4
f.__next__()
# None
# 4
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# StopIteration
第一次使用next方法时,停止在yield。yield返回0的平方,并且a没有被赋值;
第二次使用next方法时,从print(a)开始,由于a没有被赋值,因此输出None,其次print输出0的平方,i加1,最后停止在yield,返回1的平方;
第三次使用next方法时,从print(a)开始,由于a没有被赋值,因此输出None,其次print输出1的平方,i加1,最后停止在yield,返回2的平方;
第四次使用next方法时,从print(a)开始,a同样未被赋值,因此输出None,其次print输出2的平方,i加1,循环结束,返回StopIteration异常。
send方法
在介绍yield时,可以发现每次在yield处停止,但是a并没有被赋值。而前面提到的send方法可以解决这个问题,send() 和next()一样,都能让生成器继续执行(遇到yield停),但send()能传一个值,这个值作为yield表达式整体的结果。举个栗子:
def generate(n):
i = 0
while i < n:
a = yield i**2
print(a)
print(i**2)
i += 1
f = generate(3)
f.__next__()
# 0
f.__next__()
# None
# 0
# 1
f.send('hi')
# hi
# 1
# 4
上面利用send方法,在第三次执行生成器时,将“hi”作为yield表达式的整体结果赋值给了a,再执行后面的语句。