前言
前面我们了解了python的迭代器,知道了迭代器的工作原理和使用方式。在python中生成器和迭代器的功能差不多,都可以被next函数迭代,都可以被for循环使用。因为生成器的本质还是一个迭代器,生成器也是一种迭代器类型,生成器类中也定义了__iter__方法和__next__方法。返回生成器同样要使用__iter__方法,迭代生成器同样要使用__next__方法。生成器就像整型、字符串等类型一样,在python中已经内置了,我们直接使用就行了不用费力的去写一个类型,但你非要自己去实现一个整型类、字符串类、生成器类也是可以的。所以生成器就是迭代器的一种实现方式(升级版),因为定义一个迭代器需要创建一个类,还要在类中实现__iter__方法和__next__方法;而定义一个生成器只需创建一个函数就行了,大大简化了实现迭代对象的成本和流程。
基本结构
def xxx():
逻辑语句(可有可无)
yield 对象
逻辑语句(可有可无)
如果一个函数中包含了yield关键字,那它就是一个生成器。生成器不能像函数一样执行,因为生成器执行时就像类的实例化一样,返回的是一个(生成器)对象。请看如下代码:
def generator():
yield 1
print(generator())
执行结果如下:
我们可以看到生成器执行后得到的是一个对象,而且这个对象的类型是生成器类型。跟类的实例化一模一样,其实就是实例化出了一个生成器对象,所以我们使用生成器要像使用一个类对象一样,要用变量来接收生成器对象。
想要迭代生成器,必须使用next函数或者send方法。next函数我们知道是python中用来迭代对象的内置函数;send方法是生成器中的实例方法,它能给生成器传值同时也会执行生成器。
yield关键字
在函数中yield可以像return一样返回对象,return返回对象后函数就结束了,但yield返回对象后函数不会结束,会保持在yield返回对象这行语句上。这时的函数处于一种既不是激活又不是结束的挂起状态,这时我们可以去执行其他的代码,其他代码执行完了还可以回来接着执行yield后面的代码。我们平时执行代码都是一个函数执行完了才能执行下一个函数,为什么生成器可以可以不用执行完就执行其他函数。其实这也很好理解,我们知道生成器看似是一个函数,执行后得到的却是一个生成器对象,而生成器的本质就是迭代器的一种实现方式。我们在定义迭代器时会设置某些属性来记录迭代状态,使迭代器在迭代一次后可以去执行其他代码,执行完其他代码后可以再回来接着迭代。你只要把yield想成是在改变生成器中的某个属性,从而记录迭代状态,就很好理解了。同时你一定不要把生成器想象成一个函数,虽然是用函数来定义生成器,但本质是迭代器对象。
当我们第一次用next函数迭代生成器时,会从生成器的第一行代码开始执行,直到遇见yield关键字时返回对象记录迭代状态并停止执行后面的代码。当我们第二次用next函数迭代生成器时,才会继续执行yield后面的代码,直到遇到下一个yield或者结束迭代。如果有下一个yield就会返回对象记录迭代状态并停止执行后面的代码,如果没有下一个yield就结束迭代。
def generator():
print('return 1')
yield 1
print('return 2')
yield 2
print('over')
number_generator = generator() # 生成器实例化
next(number_generator) # 第一次迭代生成器
print('执行其他代码')
next(number_generator) # 第二次迭代生成器
print('执行其他代码')
next(number_generator) # 第三次迭代生成器
执行结果如下:
前两次迭代都没有问题,第三次迭代就抛出了StopIteration错误。因为生成器中只有两个yield,所以它只能被成功迭代两次,迭代第三次时就会抛出StopIteration错误终止迭代。
如果我们需要生成器迭代1000次,那么就需要使用1000个yield。如果我们在一个函数中写1000个yield,那这个函数将非常庞大,庞大到我们不想使用生成器。所以生成器不适用于返回很多没有规律的数据,更适用于返回一系列有规律的数据,这一点迭代器也一样。如果是有规律的数据我们就可以使用while循环或者for循环,再通过规律中提取出来的算法就能不断返回数据,而且定义生成器的函数也不用很庞大。例如斐波那契数列的生成:
def fibonacci(number: int):
"""
生成number个斐波那契数\n
:param number: 个数
:return: Generator
"""
if number < 1:
raise ValueError('number cannot be less than 1')
yield 1
front = 0
back = 1
state = 1
while state < number:
yield front + back
front, back = back, front + back
state += 1
for i in fibonacci(20):
print(i, end=' ')
执行结果如下:
例如生成一系列素数:
def primes_generator(n):
"""
素数生成器\n
:param n: 生成的最后一个素数小于等于n
:return: Generator
"""
yield 2
primes = list(range(3, n + 1, 2))
while primes:
yield primes[0]
primes = [i for i in primes if i % primes[0] > 0]
for i in primes_generator(100):
print(i, end=' ')
执行结果如下:
实例方法
生成器中提供了3个实例方法,分别是send、close和throw。
send方法
send方法是生成器类中的实例方法,send和next都可以迭代生成器。它们的区别是send可以接收一个参数,并把这个参数传递给yield语句,所以yield语句前面是可以存在变量和赋值运算符的。就像下面这样:
def xxx():
逻辑语句(可有可无)
value = yield 对象
逻辑语句(可有可无)
因为yield语句需要在执行结束后才能把send传递过来的值赋给变量,所以使用send第一次迭代生成器时必须传入None。如果第一次迭代传入的值不为None,就需要完成赋值。但是yield不能立即执行完,必须等到下一个send或next才能执行结束,所以此时是无法完成赋值的。所以python规定使用send第一次迭代生成器时必须传入None。就相当于第一个send是用来启动第一个yield的,第一个yield并不能接收第一个send传过来的值。请看如下代码:
def generator():
print('start')
value = yield 1
print(f'第一个yield结束: value={value}')
value = yield 2
print(f'第二个yield结束: value={value}')
number_generator = generator()
number_generator.send('hello')
print(f'返回值 number={number}')
执行结果如下:
我们第一次迭代就用send传入非None的值,python直接报错了连生成器的第一行代码都不会执行,报错显示无法将非None变量发送到刚启动的生成器。所以我们第一次迭代生成器要么直接使用next,要么就使用send(None)。
def generator():
print('start')
value = yield 1
print(f'第一个yield结束: value={value}')
value = yield 2
print(f'第二个yield结束: value={value}')
number_generator = generator()
number = number_generator.send(None)
print(f'返回值 number={number}')
number = number_generator.send('hello')
print(f'返回值 number={number}')
number = number_generator.send('world')
执行结果如下:
现在就可以正常迭代了。
close方法
close方法是生成器类中的实例方法,是用来关闭生成器的,可以在生成器未迭代完之前就直接跳转到迭代结束的状态。
def generator():
print('start')
value = yield 1
print(f'第一个yield结束: value={value}')
value = yield 2
print(f'第二个yield结束: value={value}')
number_generator = generator()
number = number_generator.send(None)
print(f'返回值 number={number}')
number_generator.close() # 提前结束迭代
number = number_generator.send('hello')
print(f'返回值 number={number}')
执行结果如下:
我们可以看到close提前把生成器的状态改为了结束状态,我们想要继续迭代时就会抛出StopIteration错误,说明生成器的迭代周期已经结束。
throw方法
throw方法是生成器类中的实例方法,用来在生成器中抛出指定的错误。throw可以接收3个参数,第一个为错误类型、第二个为错误信息、第三个为错误回溯类型。
错误类型就是Exception以及它的子类ValueError、TypeError、StopIteration等等,或者我们通过继承Exception来自定义的错误类型。
错误信息就是一段字符串,用来告诉别人为什么要抛出错误,解释程序出错的原因。
错误回溯类型就是通过获得堆栈信息,在抛出错误时得到错误的代码行、错误类型、错误信息,这个我们不用传参,就用python默认的错误回溯就行。
我们可以使用throw方法抛出错误,达到跳过某些迭代值的作用。
import sys
import traceback
def generator():
n = 1
while True:
try:
yield n
except Exception:
exc_type, exc_value, exc_traceback = sys.exc_info()
traceback.print_exception(exc_type, exc_value, exc_traceback)
n += 1
number_generator = generator()
print(next(number_generator))
number_generator.throw(ValueError, '故意抛出错误')
print(next(number_generator))
print(next(number_generator))
执行结果如下:
通过故意抛出错误就把2给跳过了,但是要记得捕获yield语句的错误,不然程序就直接停止了。
结语
其实生成器也不是什么复杂的东西,真正复杂的是能持续产生数据的算法。只要我们能从一些事物之间找到他们的联系,我们再把这种联系转换成一种算法,就可以根据一种事物推算出另一种事物,再从另一种事物推算出下一种事物,一直推算到算法的终结。就像科学家每天都在不断的研究,其本质就是为了找出万事万物之间的联系。爱因斯坦穷其一生都想找出能统一解释宇宙的规律,最后推出了相对论。到了现在科学家们又推出了弦理论,如果有一天我们真能找到一种能和万事万物都产生联系的算法,那么我们只需要一个最基本的变量,就能推算出万事万物了,那不就又是一个宇宙了吗?或许我们的宇宙就是如此,只是不知这个基本变量是什么呢,光子、弦、暗物质?