generator.throw() 有啥用?

Posted

技术标签:

【中文标题】generator.throw() 有啥用?【英文标题】:What is generator.throw() good for?generator.throw() 有什么用? 【发布时间】:2012-07-14 04:42:02 【问题描述】:

PEP 342 (Coroutines via Enhanced Generators) 向生成器对象添加了一个throw() 方法,它允许调用者在生成器内部引发异常(就好像它是由yield 表达式引发的一样)。

我想知道这个功能的用例是什么。

【问题讨论】:

上下文:我目前正在开发 php 中的生成器/协程实现,我想知道是否应该包含 throw() 功能。 你想要生成器还是协程?虽然 Python 将两者混为一谈,并且您可以从后者构建前者,但它们是不同的(就像在一个完全不同的联盟中一样)。 除其他外,这允许实现@contextmanager装饰器。 【参考方案1】:

假设我使用生成器来处理向数据库添加信息;我使用它来存储网络接收到的信息,并且通过使用生成器,我可以在我实际接收到数据时高效地执行此操作,并在其他情况下执行其他操作。

所以,我的生成器首先打开了一个数据库连接,每次你给它发送一些东西,它都会添加一行:

def add_to_database(connection_string):
    db = mydatabaselibrary.connect(connection_string)
    cursor = db.cursor()
    while True:
        row = yield
        cursor.execute('INSERT INTO mytable VALUES(?, ?, ?)', row)

这一切都很好;每次我.send()我的数据时,它都会插入一行。

但是如果我的数据库是事务性的呢?何时将数据提交到数据库时,如何向此生成器发出信号?何时中止交易?此外,它正在与数据库保持一个开放的连接,也许我有时希望它关闭该连接以回收资源。

这就是.throw() 方法的用武之地;使用.throw(),我可以在该方法中引发异常以指示某些情况:

def add_to_database(connection_string):
    db = mydatabaselibrary.connect(connection_string)
    cursor = db.cursor()
    try:
        while True:
            try:
                row = yield
                cursor.execute('INSERT INTO mytable VALUES(?, ?, ?)', row)
            except CommitException:
                cursor.execute('COMMIT')
            except AbortException:
                cursor.execute('ABORT')
    finally:
        cursor.execute('ABORT')
        db.close()

生成器上的.close() 方法基本上做同样的事情;它使用GeneratorExit 异常结合.throw() 来关闭正在运行的生成器。

所有这些都是协程如何工作的重要基础;协程本质上是生成器,加上一些额外的语法可以使编写协程更容易和更清晰。但在引擎盖下,它们仍然建立在相同的屈服和发送之上。当您并行运行多个协程时,如果其中一个协程失败,您需要一种方法来干净地退出这些协程,仅举一个例子。

【讨论】:

感谢您的回答。这绝对是一个有趣的用例。但我想知道这是否可以归类为异常滥用。提交和中止不是异常情况,而是通常行为的一部分。所以这里的异常基本上是用来改变控制流的。 @NikiC 您的观点对于同步编程是有效的,但您需要在异步编程的世界中查看这一点。想象一下上面的 try 块要大得多(在一般用例中调用 try 中的代码),甚至可能会抛出更多的 yield 语句,以便生成器在其一般用例期间进入和退出。 .throw() 方法允许我们“突围”来处理特殊异常。如果您熟悉中断处理程序,您可以这样想。这样,无论在用例中的哪个位置,我们都可以中断流程以执行特殊(如果不是关键)操作 @NikiC 控制流使用异常并没有错。 @NikiC:Python 一直在控制流中使用异常:参见前面提到的 GeneratorExit 异常。虽然 C++ 和 Java 等语言鼓励人们将异常的使用限制在真正异常的情况下,但 Python 确实更多地使用它们——但通常是通过定义的接口。 我假设 throw() 或 close() 发生在 cursor.execute() 中?这将导致 ValueError: Generator not running 而不是 Generator Exit 不管你输入了什么错误。为什么不直接引发错误?【参考方案2】:

在我看来,throw() 方法之所以有用有很多原因。

    对称性:没有充分的理由只在调用者中处理异常情况而不在生成器函数中处理。 (假设从数据库读取值的生成器返回了一个错误值,并且假设只有调用者知道该值是错误的。使用throw() 方法,调用者可以向生成器发出信号,表明存在异常情况,必须更正。)如果生成器可以引发异常,被调用者拦截,反过来也应该是可能的。

    灵活性:一个生成器函数可能有多个yield 语句,调用者可能不知道生成器的内部状态。通过抛出异常,可以重置生成器到已知状态,或者实现更复杂的流控制,单独使用next()send()close() 会更加麻烦。

重置内部状态的示例:

def gen():
    try:
        yield 10
        print("State1")
        yield 20
        print("State2")
        yield 30
        print("State3")
    
   except:
        #Reset back to State1!
        yield gen()

g = gen()
print(next(g))
print(next(g))
g = g.throw(ValueError) #state of g has been reset
print(next(g))

>>10
>>State1
>>20
>>10

询问用例可能会产生误导:对于每个用例,您都可以在不需要throw() 方法的情况下生成一个反例,并且讨论将永远持续下去......

【讨论】:

能否举个例子,throw 用于将生成器重置为已知状态?【参考方案3】:

一个用例是在发生异常时在堆栈跟踪中包含有关生成器内部状态的信息——否则调用者将看不到这些信息。

例如,假设我们有一个像下面这样的生成器,其中我们想要的内部状态是生成器的当前索引号:

def gen_items():
    for i, item in enumerate(["", "foo", "", "foo", "bad"]):
        if not item:
            continue
        try:
            yield item
        except Exception:
            raise Exception("error during index: %d" % i)

以下代码不足以触发额外的异常处理:

# Stack trace includes only: "ValueError: bad value"
for item in gen_items():
    if item == "bad":
        raise ValueError("bad value")

但是,以下代码确实提供了内部状态:

# Stack trace also includes: "Exception: error during index: 4"
gen = item_generator()
for item in gen:
    if item == "bad":
        gen.throw(ValueError, "bad value")

【讨论】:

【参考方案4】:

这个“答案”更像是一个琐事。

我们可以(ab)使用生成器的 throw() 在 lambda 中引发异常,否则它不支持 raise 语句。

foo = lambda: (_ for _ in ()).throw(Exception('foobar'))

引自https://***.com/a/8294654/728675

【讨论】:

以上是关于generator.throw() 有啥用?的主要内容,如果未能解决你的问题,请参考以下文章

用eclipse编写android项目时,项目中的gen,assets,bin,res和res里面的三个文件都是具体干啥用的?

dowhile(0) 有啥用? [复制]

DataContext 有啥用?

UnmanagedMemoryStream 有啥用?

appStoreReceiptURL 有啥用?

有啥用?和:[重复]