Python 生成器与它的 send,throw,close 方法

Python 生成器与它的 send,throw,close 方法

转载请注明出处:https://blog.csdn.net/jpch89/article/details/87036970



0. 参考资料


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.6from 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),这意味着最好不要在相应的 exceptfinally 中写 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

猜你喜欢

转载自blog.csdn.net/jpch89/article/details/87036970