生成器与 yield 语句
生成器
有些情况下,序列或集合内的元素的个数非常巨大,如果全制造出来并放入内存,对计算机的压力是非常大的。
如果元素可以按照某种算法推算出来,需要就计算到哪个,就可以在循环的过程中不断推算出后续的元素,而不必创建完整的元素集合,从而节省大量的空间。
在Python中,这种一边循环一边计算出元素的机制,称为生成器:generator。
生成器的优点
- 节省内存。生成器只有在用的时候会生成,所以,当我们没有用到这个列表的时候它就不会存在,也不会占内存
- 节省代码,减少代码量同时提高代码可读性。如下:同样的功能,但是使用生成器只用一句代码
1. 生成器推导式
2. yield 关键字
在 Python中,使用yield返回的函数会变成一个生成器(generator)。 在调用生成器的过程中,每次遇到yield时函数会暂停并保存当前所有的运行信息,返回yield的值。并在下一次执行next()方法时从当前位置继续运行。
在 满足判断条件的情况下,直接 return 了,生成器没有产生值,所以再次调用的时候引发 StopIteration 异常。
-
在一个函数体内使用 yield 表达式会使这个函数变成一个生成器,并且在一个 async def 定义的函数体内使用 yield 表达式会让协程函数变成异步的生成器。
-
python 官方文档对于生成器调用机制的解释如下:
当一个生成器函数被调用的时候,它返回一个迭代器,称为生成器。然后这个生成器来控制生成器函数的执行。当这个生成器的某一个方法被调用的时候,生成器函数开始执行。这时会一直执行到第一个 yield 表达式,在此执行再次被挂起,给生成器的调用者返回 expression_list 的值。挂起后,我们说所有局部状态都被保留下来,包括局部变量的当前绑定,指令指针,内部求值栈和任何异常处理的状态。通过调用生成器的某一个方法,生成器函数继续执行。此时函数的运行就和 yield 表达式只是一个外部函数调用的情况完全一致。恢复后 yield 表达式的值取决于调用的哪个方法来恢复执行。 如果用的是 next() (通常通过语言内置的 for 或是 next() 来调用) 那么结果就是 None. 否则,如果用 send(), 那么结果就是传递给send方法的值。
3. 生成器 - 迭代器的方法
在生成器已经在执行时调用以下任何方法都会引发 ValueError 异常
3.1 genetator.next()
开始一个生成器函数的执行或是从上次执行的 yield 表达式位置恢复执行。 当一个生成器函数通过 next() 方法恢复执行时,当前的 yield 表达式总是取值为 None。 随后会继续执行到下一个 yield 表达式,其 expression_list 的值会返回给 next() 的调用者。 如果生成器没有产生下一个值就退出,则将引发 StopIteration 异常。
此方法通常是隐式地调用,例如通过 for 循环或是内置的 next() 函数。
如下所示:
while循环是用来确保生成器函数永远也不会执行到函数末尾的。只要调用next()这个生成器就会生成一个值。
如果没有 while 循环,那么再第二次调用的时候就会引发一个 StopIteration 异常。
3.2 generator.send(value)
恢复执行并向生成器函数“发送”一个值。 value 参数将成为当前 yield 表达式的结果。
send() 方法会返回生成器所产生的下一个值,或者如果生成器没有产生下一个值就退出则会引发 StopIteration。
当调用 send() 来启动生成器时,它必须以 None 作为调用参数,因为这时没有可以接收值的 yield 表达式。如果传入一个真实的值来启动,则会报错:
3.3 generator.throw(type[, value[, trace])
在生成器暂停的位置引发 type 类型的异常,并返回该生成器函数所产生的下一个值。
如果生成器没有产生下一个值就退出,则将引发 StopIteration 异常。
如果生成器函数没有捕获传入的异常,或引发了另一个异常,则该异常会被传播给调用者。
3.4 generator.close()
close 方法允许任何挂起的 finally 子句执行。
如果生成器已经由于异常或正常退出则 close() 不会做任何事。
如下所示,生成器没有启动,调用 close 则不会做任何事:
如下所示,生成器已经启动,调用 close 则会执行 finally 子句。
4. 生成器的使用例子
假如:要你实现一个获取所有素数的功能,参数是一个 list,且数据都是 int 类型.
然后写出里了下面的代码
import math
def get_primes(input_list):
# return (element for element in input_list if is_prime(element))
resul = []
for i in input_list:
if is_prime(i):
resul.append(i)
return resul
# 或者更好一点的方式
def get_primes(input_list):
return (element for element in input_list if is_prime(element))
def is_prime(n):
if n == 1:
return False
for i in range(2, int(math.sqrt(n))+1):
if n % i == 0:
return False
return True
但是如果这个 list 非常大,创建这个 list 就会耗费非常大的内存,这个时候你要怎么优化?这个时候就可以使用 生成器 来实现了。
前面介绍了,在一个函数里加上 yield 语句,那么这个函数就会变成生成器。代码如下:这段代码,通过 while True 的循环,创建了一个无穷序列。
def get_primes(number):
while True:
if is_prime(number):
yield number
number += 1
def solve_number_10():
total = 0
for next_prime in get_primes(3):
if next_prime < 20:
print(next_prime)
total += next_prime
else:
print(total)
return
上述代码,调用 get_primes(3) 方法,那么将从 3 开始,获取所有素数。如果素数小于 20 ,那么就进行累加。否则 return 结束程序。
再看下面这个例子:
import random
def get_data():
"""返回0到9之间的3个随机数"""
return random.sample(range(10), 3)
def consume():
"""显示每次传入的整数列表的动态平均值"""
running_sum = 0
data_items_seen = 0
while True:
data = yield
data_items_seen += len(data)
running_sum += sum(data)
print('The running average is {}'.format(running_sum / float(data_items_seen)))
def produce(consumer):
"""产生序列集合,传递给消费函数(consumer)"""
while True:
data = get_data()
print('Produced {}'.format(data))
consumer.send(data)
yield
if __name__ == '__main__':
consumer = consume()
consumer.send(None)
producer = produce(consumer)
for _ in range(10):
print('Producing...')
next(producer)
代码运行过程:
- 首先 定义一个 consumer 生成器
- 通过 send 方法启动 consumer 生成器,在遇到 yield 时函数被挂起
- 定义一个 producer 生成器, 循环 10 次,调用 next 方法启动 producer 生成器, 运行过程如下:
- producer 获得 3 个随机数的序列
- consumer.send(data) 调用 consumer 生成器,从上次被挂起的地方继续执行 consumer,consumer 运行完 print 之后,因为有 while True 语句,所以会继续循环遇到了 data = yield 语句,然后 consumer 被挂起。继续执行 producer,producer 遇到了 yield 语句,然后被挂起
- 继续执行 next(producer) 循环上面的过程
这个过程实现了一个协程,这样可以模拟一个伪并发。
协程就是你可以暂停执行的函数"。也就是yield。当一个函数在执行过程中被阻塞时,就用yield挂起,然后执行另一个函数。当阻塞结束后,可以用next()或者send()唤醒。
相比多线程,协程的好处是它在一个线程内执行,避免线程之间切换带来的额外开销,而且协程不存在加锁的步骤。