请看代码片段:
def mm(n):
result = []
for e in range(n):
result.append(e)
return result
if __name__=='__main__':
for p in mm(100):
print(p)
这个例子用来输出0~99共100个整数。代码看起来没有任何问题,但是如果把常数100改为1亿,则运行时要过很久才会打印出数据,更重要的是,内存中要准备好1亿个单位空间用来存放这1亿个数。解决问题的办法是使用yield语句:
def mm(n):
for e in range(n):
yield e
if __name__=='__main__':
for p in mm(100000000):
print(p)
运行这段代码,程序会很快打印数据,更重要的是,内存耗费也几乎没有增加。
类似于 return语句,yield <表达式>语句计算并且返回表达式的结果。Python把含有yield语句的函数称为生成器(Generator)。调用普通函数时会导致Python跳转到相应的函数体继续运行,而调用生成器函数则会得到一个生成器对象,而不是运行相应的函数体。比如上述代码中调用mm (100000000)并不会导致mm()函数体的执行,而是生成了一个能产生1亿个数的生成器对象[1]。
当把一个生成器对象置于for循环中,就像上述代码中主程序那样,则每一次循环都会导致该生成器输出一个整数,并且该整数被赋值给循环变量。代码
for p in mm(10):
print(p)
等价于:
genr = mm(10)
while True:
try:
p = next(genr)
except StopIteration:
break
print(p)
也就是说,生成器genr在for循环中的使用,本质上是把genr作为参数调用内置函数next()。对于mm (10)所产生的生成器对象来说,next()最多只能调用10次。当第11次调用next()函数时就会产生StopIteration错误。Python用try ... except语句捕捉这个错误,一旦捕到,就执行break语句跳出循环。
是next(genr)语句而不是mm(10)才导致Python调用mm()的函数体,此时,运行堆栈(参见https://fanglin.blog.csdn.net/article/details/119202980)中会被压入一条运行记录rec,就像正常函数调用一样。当Python在mm()的函数体中执行到yield e语句时,Python会暂停在这一句之后,然后把运行记录rec保存在生成器的一个私有成员变量中,同时保存的还有代码的当前位置(即yield语句之后)。接着,Python弹出rec,把e作为返回值返回,就像return e所做的那样。于是调用方p=next(genr)就得到了这个返回值并赋值给变量p。
当再次执行next(genr)时,Python首先在运行堆栈中恢复rec,然后程序从上次暂停的地方(即yield e之后)继续运行。当再次遇到yield语句时重复上述过程,从而保证每生成一个整数,就输出一个,而不是等全部10个整数都生成完毕再一次性返回。
2. 双向的生成器
生成器所产生的数据可以从生成方向使用方传递,而使用方也可以向生成方传递数据。方法是这样的:生成器执行形如abc = yield 123的语句,一方面输出123,另一方面接收使用方发出的数据,并且赋值给变量abc。而使用方应该执行xyz=genr.send(789),一方面接收数据123,另一方面发送数据789。请看下面代码:
双向发送数据示例
def my_genr(n):
for i in range(n):
a = yield i
print('生成器收到:', a)
genr = my_genr(4)
x = 100
while True:
b = genr.send(x) # 第一次运行到这里就会报错
x += 1
print('使用方收到:', b)
if b == 3:
break
我们期望上述代码中的生成器能收到100~103四个整数,而使用方收到0~3四个整数,但实际运行时,第一次执行到genr.send(x)时就报错:can't send non-None value to a just-started generator。即不能发送一个非None的值给一个刚刚启动的生成器。原因在于genr.send()与next(genr)一样,都会导致生成器genr的函数体被执行,即进行一次函数调用。当Python在生成器函数体中遇到第一个yield语句时,会把它理解为返回语句return,不同点在于Python还把位于运行堆栈栈顶的运行记录和当前代码位置(即yield语句之后)保存在生成器中。所以形如abc=yield 123的语句等价于:
<保存栈顶运行记录>
保存下面return之后的语句位置
return 123
abc = <使用方发来的数据>
也就是说,在对变量abc赋值之前,函数已经退出了。这就是为什么上述genr.send()语句会报错的原因:此时发送的数据yield语句是接收不到的。纠正这个错误的方法有两种:令第一个.send()语句发送None,或者在.send()之前调用next(),即接收数据但不发送数据:
双向发送数据的正确做法
def my_genr(n):
yield None # 抵消第一个.send()或者next()调用
for i in range(n):
a = yield i
print('生成器收到:', a)
genr = my_genr(4)
x = 100
next(genr) # 保证生成器中的yield语句能接收到下面发送的数据
while True:
b = genr.send(x) # 不会报错
x += 1
print('使用方收到:', b)
if b == 3:
break
运行结果:
使用方收到: 0
生成器收到: 101
使用方收到: 1
生成器收到: 102
使用方收到: 2
生成器收到: 103
使用方收到: 3
3. yield from语句
有时,一个函数本身没有yield语句,但是它调用的一个子函数却含有yield语句,此时,从逻辑上前,当前函数也是会调用到yield语句的,但是它却不被认为是一个生成器,因为它的函数体代码中没有出现yield关键字。
这是因为Python是一种解释运行的语言,子函数在被调用前是不会被编译,不会被检查的。所以只有当当前函数运行到调用子函数时,Python才会去检查子函数的代码,才会发现它是一个生成器。
为了避免这个问题,Python允许用户使用yield from语句调用子函数,从而使得Python能够发现当前函数是一个生成器。比如:
def mm1():
print('in mm1()')
yield from mm2()
def mm2():
yield 123
yield 456
for e in mm1():
print(e)
程序输出:
in mm1()
123
456
[1] 如果用参数1亿再一次调用mm()函数就会产生一个新的生成器对象,该对象同样能产生1亿个整数,但是与前一个相比是两个不同的生成器对象。