PEP 342(通过增强生成器实现的协程)为生成器对象添加了一个 throw()
方法,它允许调用者在生成器内部抛出异常(就像由 yield
表达式抛出一样)。
我想知道这个特性的使用场景是什么。
PEP 342(通过增强生成器实现的协程)为生成器对象添加了一个 throw()
方法,它允许调用者在生成器内部抛出异常(就像由 yield
表达式抛出一样)。
我想知道这个特性的使用场景是什么。
假设我使用生成器来处理添加信息到数据库的操作;我用它来存储从网络接收的信息,并通过使用生成器,只有在实际接收到数据时才能高效地执行此操作,并在其他情况下执行其他操作。
因此,我的生成器首先打开数据库连接,每次你发送一些东西时,它都会添加一行:
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()
来关闭正在运行的生成器。
所有这些是协程工作方式的重要基础;协程本质上是生成器,加上一些额外的语法使编写协程更加容易和清晰。但在幕后,它们仍然建立在相同的 yield 和 send 上。当您并行运行多个协程时,如果其中一个协程失败,您需要一种方法来干净地退出这些协程,这只是一个例子。
在我看来,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
可以将生成器重置到已知状态? - astralwolf一个使用案例是在异常发生时,在堆栈跟踪中包含有关生成器内部状态的信息 -- 否则调用者将看不到这些信息。
例如,假设我们有一个生成器,如下所示,其中我们想要的内部状态是生成器的当前索引号:
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")
throw()
在lambda中抛出异常,这样就可以使用不支持raise
语句的lambda了。foo = lambda: (_ for _ in ()).throw(Exception('foobar'))
在IT技术中,“MVC”代表“模型-视图-控制器”。它是一种软件设计模式,旨在将应用程序的不同方面分离开来。模型表示应用程序中的数据和业务逻辑,视图表示用户界面,而控制器则处理输入并根据模型和视图执行相应操作。使用MVC可以增强代码的可维护性和重用性,提高开发效率,并支持团队协作。我正在使用它来编写可重用的库代码,可以同时具有同步和异步代码路径。它被简化为类似于这样的形式:
from abc import ABCMeta, abstractmethod
from typing import Generator
class Helper( metaclass = ABCMeta ):
@abstractmethod
def help( self, con: DatabaseConnection ) -> None:
raise NotImplementedError
@abstractmethod
async def ahelp( self, con: AsyncDataConnection ) -> None:
raise NotImplementedError
class HelperSelect( Helper ):
' logic here to execute a select query against the database '
rows: list[dict[str,Any]] # help() and ahelp() write their results here
def help( self, con: DatabaseConnection ) -> None:
assert False, 'TODO FIXME write database execution logic here'
async def ahelp( self, con: AsyncDataConnection ) -> None:
assert False, 'TODO FIXME write database execution logic here'
def _application_logic() -> Generator[Helper,None,int]:
sql = 'select * from foo'
helper = HelperSelect( sql )
yield helper
# do something with helper.rows
return 0
def do_something( con: DatabaseConnection ):
gen = _application_logic()
try:
while True:
helper = next( gen )
try:
helper.help( con )
except Exception as e:
gen.throw( e )
except StopIteration as e:
return e.value
async def ado_something( con: AsyncDatabaseConnection ):
gen = _application_logic()
try:
while True:
helper = next( gen )
try:
await helper.ahelp( con )
except Exception as e:
gen.throw( e )
except StopIteration as e:
return e.value
如果不使用gen.throw,堆栈跟踪就不会显示异常发生的逻辑位置,这可能非常令人沮丧。使用上面示例中的gen.throw()可以解决这个问题。
Helper类的原因是,除了数据库查询之外,逻辑可能需要请求半打不同类型的事情,需要异步执行。
我采用了伪代码,并构建了一个可以实际运行并查看差异的版本:
from abc import ABCMeta, abstractmethod
import logging
from typing import Any, Generator
class Helper( metaclass = ABCMeta ):
@abstractmethod
def help( self ) -> None:
raise NotImplementedError
class HelperBoom( Helper ):
def help( self ) -> None:
assert False
def _application_logic() -> Generator[Helper,None,int]:
helper = HelperBoom()
yield helper
return 0
def do_something1():
gen = _application_logic()
try:
while True:
helper = next( gen )
helper.help()
except StopIteration as e:
return e.value
def do_something2():
gen = _application_logic()
try:
while True:
helper = next( gen )
try:
helper.help()
except Exception as e:
gen.throw( e )
except StopIteration as e:
return e.value
try:
do_something1()
except Exception:
logging.exception( 'do_something1 failed:' )
try:
do_something2()
except Exception:
logging.exception( 'do_something2 failed:' )
ERROR:root:do_something1 failed:
Traceback (most recent call last):
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 35, in <module>
do_something1()
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 23, in do_something1
helper.help()
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 12, in help
assert False
AssertionError
ERROR:root:do_something2 failed:
Traceback (most recent call last):
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 39, in <module>
do_something2()
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 32, in do_something2
gen.throw( e )
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 16, in _application_logic
yield helper
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 30, in do_something2
helper.help()
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 12, in help
assert False
AssertionError
throw()
功能。 - NikiC@contextmanager
装饰器。 - Alexey