Python 生成器与它的 send,throw,close 方法
转载请注明出处:https://blog.csdn.net/jpch89/article/details/87036970
文章目录
0. 参考资料
- Generator-iterator methods
- Python特性(八):生成器对象的send方法
- Python特性(九):生成器对象的throw方法
- Python特性(十):GeneratorExit异常
- Python特性(十一):生成器对象的close方法
- Cleaning Up in a Python Generator Can Be Dangerous
1. 生成器简介
1.1 生成器的定义方式
在 Python
中,定义生成器有两种方式:
- 生成器函数
即内含yield
关键字的函数。
调用该函数,会得到一个生成器对象。 - 生成器表达式
把列表推导式的方括号改成圆括号,就成了生成器表达式。
举例如下:
from collections.abc import Generator
# 生成器函数
def fib(n):
a, b = 0, 1
i = 0
while i < n:
yield b
a, b = b, a + b
i += 1
print(isinstance(fib(1), Generator))
"""
True
"""
# 生成器表达式
g = (i for i in (1, 2, 3))
print(isinstance(g, Generator))
"""
True
"""
1.2 生成器与迭代器的关系
所有的生成器都是迭代器,因为生成器对象都有 __iter__
和 __next__
方法,且 __iter__
方法返回自身。
验证方式一:使用 isinstance
和抽象基类 collections.abc.Iterator
验证。
>>> from collections import Iterator
__main__:1: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
>>> from collections.abc import Iterator
>>> g = (i for i in '嘿嘿嘿')
>>> isinstance(g, Iterator)
True
上面的代码证明了生成器是迭代器 Iterator
的实例。
补充:
- 注意
3.7
版本如果直接从collections
模块中导入抽象基类,控制台会有废弃警告,并提示在3.8
中将完全禁止这种写法,只能写成from collections.abc import 抽象基类
。- 然而在
3.6
中from collections import Iterator
还是完全没有问题的。
验证方式二:使用 dir(g)
查看生成器 g
内部方法,并使用 iter(g)
检查返回值。
>>> dir(g)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
>>> id(g)
2161748426448
>>> id(iter(g))
2161748426448
>>> iter(g) is g
True
可见生成器对象 g
中有 __iter__
和 __next__
方法,并且 __iter__
方法返回自身。
验证方式三:使用 issubclass
判断 Generator
是否是 Iterator
的子类。
>>> from collections.abc import Generator
>>> from collections.abc import Iterator
>>> issubclass(Generator, Iterator)
True
验证方式四:使用 __bases__
属性,查看 Generator
父类,注意这个得到的是直接父类,祖先类并不会显示出来。
>>> Generator.__bases__
(<class 'collections.abc.Iterator'>,)
验证方式五:使用 __mro__
属性,查看方法解析顺序元组。与 __bases__
属性不一样的是,它会按照 C3
算法显示整个继承树。
>>> Generator.__mro__
(<class 'collections.abc.Generator'>, <class 'collections.abc.Iterator'>, <class 'collections.abc.Iterable'>, <class 'object'>)
2. 生成器对象的专属方法
在上节我们看到,生成器都是迭代器,但是生成器还有一些专属方法,都有哪些呢?可以通过差集运算来查看。
>>> set(dir(Generator)) - set(dir(Iterator))
{'close', 'send', 'throw'}
所以我们下面就来介绍这三个生成器对象的专属方法:
send
throw
close
2.1 生成器的 send 方法
2.1.1 参考帮助文档
>>> help(g.send)
Help on built-in function send:
send(...) method of builtins.generator instance
send(arg) -> send 'arg' into generator,
return next yielded value or raise StopIteration.
2.1.2 send 方法详解
generator.send(value)
- 作用:向生成器发送一个值,随后恢复执行。
value
参数是send
方法向生成器发送的值,这个值会作为当前所在的yield
表达式的结果。- 随后生成器恢复执行,直到下一个
yield
,把它后面的值作为send
方法的结果返回。
如果恢复执行后再也没有yield
语句,生成器退出,并抛出StopIteration
异常。 - 如果一开始使用
send
启动生成器,必须使用None
作为参数,因为一开始没有可以接收值的yield
表达式。
个人理解
send
三部曲:发送值、恢复执行、返回值
send
就像是升级版的next
,比起next
它多了发送值到生成器内的功能。
而next
只有两步:恢复执行、返回值
g.send(None)
和next(g)
等价,也就是说,发送一个None
相当于省去了发送值的这一步骤。
举一个例子:
import sys
def gen():
x = yield 1
print('x:', x)
y = yield 2
print('y:', y)
# 得到生成器对象 g
g = gen()
# 启动生成器(也叫激活生成器、预激生成器)
# 参数一定是 None,否则报错
# TypeError: can't send non-None value to a just-started generator
ret = g.send(None)
# 或者写成 next(g),这是激活生成器的推荐写法
print('第一次 yield 的返回值:', ret)
"""
第一次 yield 的返回值: 1
"""
print()
ret = g.send('测试')
print('第二次 yield 的返回值:', ret)
"""
x: 测试
第二次 yield 的返回值: 2
"""
print()
try:
ret = g.send(999)
except StopIteration:
exc_type, exc_value, exc_tb = sys.exc_info()
print('异常类型:%s' % exc_type)
print('异常值:%s' % exc_value)
print('异常追踪信息:%s' % exc_tb)
"""
y: 999
异常类型:<class 'StopIteration'>
异常值:
异常追踪信息:<traceback object at 0x000001B97EC00308>
"""
2.2 生成器的 throw 方法
2.2.1 参考帮助文档
>>> help(g.throw)
Help on built-in function throw:
throw(...) method of builtins.generator instance
throw(typ[,val[,tb]]) -> raise exception in generator,
return next yielded value or raise StopIteration.
2.2.2 throw 方法详解
generator.throw(type[, value[, traceback]])
- 作用:在生成器暂停的地方抛出类型为
type
的异常,并返回下一个yield
的返回值。 - 如果生成器函数没有捕获并处理传入的异常,或者说抛出了另一个异常,那么该异常会被传递给调用方。
- 如果生成器退出时还没有
yield
新值,则会抛出StopIteration
异常。
第一种情况:捕获并处理传入的异常,得到下一个 yield
的返回值。
def gen():
n = 0
while True:
try:
yield n
n += 1
except ZeroDivisionError:
print('捕获到了 ZeroDivisionError')
print('此时的 n 为:%s' % n)
g = gen()
ret = next(g)
print('第一次 yield 的返回值:%s' % ret)
"""
第一次 yield 的返回值:0
"""
print()
ret = g.throw(ZeroDivisionError)
print('第二次 yield 的返回值:%s' % ret)
"""
捕获到了 ZeroDivisionError
此时的 n 为:0
第二次 yield 的返回值:0
"""
print()
ret = next(g)
print('第三次 yield 的返回值:%s' % ret)
"""
第三次 yield 的返回值:1
"""
注意:
- 之所以第二次
yield
的返回值还是0
,是因为在第一次yield
的地方抛出了ZeroDivisionError
异常,而该异常被except
捕获,跳过了n += 1
的步骤。
在except
异常处理器中也可以看到,n
并没有改变,仍然是0
。 - 可以看到,如果通过
throw
传入的异常被捕获的话,生成器能够恢复执行直到下一个yield
。
第二种情况:没有捕获并处理 throw
传入的异常,异常会回传给调用方。
import sys
def gen():
n = 0
while True:
yield n
n += 1
g = gen()
ret1 = next(g)
print('第一次 yield 的返回值:%s' % ret1)
"""
第一次 yield 的返回值:0
"""
print()
try:
ret2 = g.throw(ZeroDivisionError) # ret2 并没有收到任何值
except ZeroDivisionError:
print('调用方捕获到 ZeroDivisionError 异常')
print(sys.exc_info())
"""
调用方捕获到 ZeroDivisionError 异常
(<class 'ZeroDivisionError'>, ZeroDivisionError(), <traceback object at 0x0000028E8AA10148>)
"""
print()
# 因为赋值没有发生就抛出了异常,所以变量 ret2 还不存在
try:
print(ret2)
except NameError:
print('捕获到了 NameError')
print(sys.exc_info())
"""
捕获到了 NameError
(<class 'NameError'>, NameError("name 'ret2' is not defined"), <traceback object at 0x000001C624DB0248>)
"""
print()
print('尝试再次从生成器中获取值')
print(next(g))
"""
尝试再次从生成器中获取值
Traceback (most recent call last):
File "test.py", line 41, in <module>
print(next(g))
StopIteration
"""
注意:
- 对于已经通过抛出异常而退出的生成器再使用
next(g)
会持续抛出StopIteration
异常。
第三种情况:生成器退出时没有 yield
新值,会抛出 StopIteration
异常。
import sys
def gen():
try:
# 注意是在当前暂停的 yield 处抛出异常
# 所以要在这里捕获
yield 1
except Exception as e:
print('在生成器内部捕获了异常')
print(e.args)
print('处理完毕,假装什么也没发生')
print()
# yield 2
g = gen()
print(next(g))
"""
1
"""
print()
g.throw(TypeError, '类型错误哟~')
"""
在生成器内部捕获了异常
('类型错误哟~',)
处理完毕,假装什么也没发生
Traceback (most recent call last):
File "test.py", line 23, in <module>
g.throw(TypeError, '类型错误哟~')
StopIteration
"""
注意:
- 虽然捕获并处理了
throw
传入的异常,但是由于处理完之后生成器没有后续语句而退出运行,而且并没有yield
新值,所以会自动抛出一个StopIteration
异常。 - 如果把
yield 2
注释打开,则不会抛出StopIteration
异常,因为此时生成器暂停并返回了2
。 - 如果
try
捕获的是yield 2
,那么实际上TypeError('类型错误哟~')
会被传递到顶层调用方。因为throw
是在当前暂停处抛出异常,也就是yield 1
语句。
2.3 生成器的 close 方法
2.3.1 参考帮助文档
>>> help(g.close)
Help on built-in function close:
close(...) method of builtins.generator instance
close() -> raise GeneratorExit inside generator.
2.3.2 close 方法详解
generator.close()
- 作用:在生成器函数暂停的地方抛出一个
GeneratorExit
异常。 - 这并不等价于
generator.throw(GeneratorExit)
,后面会说原因。 - 如果生成器抛出
StopIteration
异常(不管是由于正常退出还是因为该生成器已经关闭),或者抛出GeneratorExit
异常(不捕获该异常即可),close
方法不传递该异常,直接返回到调用方。而生成器抛出的其他异常会传递给调用方。 GeneratorExit
异常的产生意味着生成器对象的生命周期已经结束,因此生成器方法后续语句中不能再有yield
,否则会产生RuntimeError
。(而throw
方法是期待一个yield
返回值的,如果没有,则会抛出StopIteration
异常。)- 对于已经正常退出或者因为异常退出的生成器对象,
close
方法不会进行任何操作。
第一种情况:不捕获 GeneratorExit
异常,close
方法返回调用方,不传递该异常。
def gen():
print('下面 yield 1')
yield 1
print('下面 yield 2')
yield 2
g = gen()
next(g)
g.close()
"""
下面 yield 1
"""
print()
next(g)
"""
Traceback (most recent call last):
File "test.py", line 15, in <module>
next(g)
StopIteration
"""
注意:对已经关闭的生成器对象使用 next
会抛出 StopIteration
异常。
第二种情况:生成器自然退出抛出 StopIteration
异常,该异常不会传递给调用方,close
方法正常返回。
def gen():
try:
yield 1
except GeneratorExit:
print('捕获到GeneratorExit')
print('生成器函数结束了')
g = gen()
print(next(g))
g.close()
"""
1
捕获到GeneratorExit
生成器函数结束了
"""
第三种情况:在 GeneratorExit
抛出后还有 yield
语句,会产生 RuntimeError
。另外生成器对象被垃圾回收时,解释器会自动调用该对象的 close
方法(PEP 342
),这意味着最好不要在相应的 except
和 finally
中写 yield
语句,否则不知道什么时候就会抛出 RuntimeError
异常。
def gen():
try:
yield 1
except GeneratorExit:
print('捕获到 GeneratorExit')
print('尝试在 GeneratorExit 产生后 yield 一个值')
yield 2
print('生成器结束')
g = gen()
next(g)
g.close()
"""
捕获到 GeneratorExit
尝试在 GeneratorExit 产生后 yield 一个值
Traceback (most recent call last):
File "test.py", line 14, in <module>
g.close()
RuntimeError: generator ignored GeneratorExit
"""
一种防止抛出 RuntimeError
的安全生成器写法:设置一个布尔标识。
def safegen():
yield 'so far so good'
closed = False
try:
yield 'yay'
except GeneratorExit:
closed = True
raise
finally:
if not closed:
yield 'boo'
第四种情况:对已经关闭的生成器对象调用 close()
方法,不会进行任何操作。
def gen():
yield 1
print('我不会被执行')
print('因为在 yield 1 就抛出了 GeneratorExit 异常')
print('未经捕获的 GeneratorExit 异常不会传递')
print('返回执行权给 close 的调用方')
g = gen()
g.close()
g.close()
g.close() # 多次调用 close,什么效果都没有
补充:GeneratorExit
异常只有在生成器对象被激活后,才有可能产生。
def gen():
try:
yield 1
except GeneratorExit:
print('捕获到 GeneratorExit')
raise
g1 = gen()
next(g1)
g1.close()
"""
捕获到 GeneratorExit
"""
# 没有激活生成器,就不会触发 GeneratorExit 异常
print()
g2 = gen()
g2.close()
print('脚本运行完毕')
"""
脚本运行完毕
"""
完成于 2019.02.12