2012-07-14 128 views
41

PEP 342 (Coroutines via Enhanced Generators)增加了一个throw()方法到发生器对象,它允许调用者在里面产生一个异常这个发生器(就像它被yield表达式抛出一样)。generator.throw()的优点是什么?

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

+1

上下文:我目前正在PHP中使用generator/coroutine实现,我想知道是否应该包含'throw()'功能。 – NikiC 2012-07-14 16:51:48

+3

你想要发电机或协同?虽然Python将这两者混为一谈,而你可以从后者中构建前者,但它们是不同的(如同一个完全不同的联盟)。 – delnan 2012-07-14 17:43:51

回答

47

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

所以,我的发电机首先打开一个数据库连接,以及每次发送的东西,它会添加一行:

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()关闭正在运行的发电机。

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

+6

感谢您的回答。这绝对是一个有趣的用例。但我想知道这是否可以归类为异常滥用。承诺和放弃不是特殊的条件,而是通常行为的一部分。所以在这里,异常基本上被用作改变控制流的手段。 – NikiC 2012-07-14 21:06:18

+2

@NikiC您的观点适用于同步编程,但您需要在异步编程的世界中查看这一点。想象一下,上面的try块更大一些(在try中调用代码,一般用例),甚至可以抛出更多的yield语句,以便生成器在其一般用例中进入和退出。 .throw()方法允许我们“爆发”来处理特殊的异常。如果你熟悉中断处理程序,你可以这样想。这样,无论在哪个用例中,我们都可以中断流程以执行特殊的(如果不是关键的)操作 – 2012-07-18 22:58:29

+3

@NikiC对控制流程使用异常没有任何问题。 – Marcin 2012-07-19 17:18:35

9

在我看来,throw()方法是有用的,原因很多。

  1. 对称性:没有强有力的理由应该只在调用者而不是在生成器函数中处理异常情况。 (假设发电机读取数据库中的数据返回一个错误的值,并且假设只有主叫方知道这个数值是错误的。使用方法,主叫方可以向发电机发出信号,指出存在需要校正的异常情况)如果发生器可以引发由调用者拦截的异常,则反向也应该是可能的。

  2. 灵活性:生成器函数可能有多个yield语句,并且调用者可能不知道生成器的内部状态。通过抛出异常,可以将发生器重置为已知状态,或者实现更复杂的流量控制,这将更加繁琐,单独使用next(),send(),close()

询问使用情况下,可能会产生误导:每次使用情况下,你可能会产生一个反例,而不需要一个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")