如何从异步处理程序的信号处理程序中捕获自定义异常?

问题描述

使用asyncio时,从信号处理程序回调中抛出自定义异常时遇到问题。

如果我从下面的ShutdownApp中扔出do_io(),我就能在run_app()中正确地抓住它。但是,当从handle_sig()引发异常时,我似乎无法抓住它。

Minimal,Reproducible Example使用Python 3.8.5测试:

import asyncio
from functools import partial
import os
import signal
from signal import Signals


class ShutdownApp(BaseException):
    pass


os.environ["PYTHONASYNCIODEBUG"] = "1"


class App:
    def __init__(self):
        self.loop = asyncio.get_event_loop()

    def _add_signal_handler(self,signal,handler):
        self.loop.add_signal_handler(signal,handler,signal)

    def setup_signals(self) -> None:
        self._add_signal_handler(signal.SIGINT,self.handle_sig)

    def handle_sig(self,signum):
        print(f"\npid: {os.getpid()},Received signal: {Signals(signum).name},raising error for exit")
        raise ShutdownApp("Exiting")

    async def do_io(self):
        print("io start. Press Ctrl+C Now.")
        await asyncio.sleep(5)
        print("io end")

    def run_app(self):
        print("Starting Program")
        try:
            self.loop.run_until_complete(self.do_io())
        except ShutdownApp as e:
            print("ShutdownApp caught:",e)
            # Todo: do other shutdown related items
        except:
            print("Other error")
        finally:
            self.loop.close()


if __name__ == "__main__":
    my_app = App()
    my_app.setup_signals()
    my_app.run_app()
    print("Finished")

在异步调试模式下按CTRL+C(对于SIGINT)之后的输出

(env_aiohttp) anav@anav-pc:~/Downloads/test$ python test_asyncio_signal.py 
Starting Program
io start. Press Ctrl+C Now.
^C
pid: 20359,Received signal: SIGINT,raising error for exit
Exception in callback App.handle_sig(<Signals.SIGINT: 2>)
handle: <Handle App.handle_sig(<Signals.SIGINT: 2>) created at /home/anav/miniconda3/envs/env_aiohttp/lib/python3.8/asyncio/unix_events.py:99>
source_traceback: Object created at (most recent call last):
  File "test_asyncio_signal.py",line 50,in <module>
    my_app.setup_signals()
  File "test_asyncio_signal.py",line 25,in setup_signals
    self._add_signal_handler(signal.SIGINT,self.handle_sig)
  File "test_asyncio_signal.py",line 22,in _add_signal_handler
    self.loop.add_signal_handler(signal,signal)
  File "/home/anav/miniconda3/envs/env_aiohttp/lib/python3.8/asyncio/unix_events.py",line 99,in add_signal_handler
    handle = events.Handle(callback,args,self,None)
Traceback (most recent call last):
  File "/home/anav/miniconda3/envs/env_aiohttp/lib/python3.8/asyncio/events.py",line 81,in _run
    self._context.run(self._callback,*self._args)
  File "test_asyncio_signal.py",line 31,in handle_sig
    raise ShutdownApp("Exiting")
ShutdownApp: Exiting
io end
Finished

预期输出

Starting Program
io start. Press Ctrl+C Now.
^C
pid: 20359,raising error for exit
ShutdownApp caught: Exiting
io end
Finished

是否可以从asyncio中的信号处理程序引发自定义异常?如果是这样,我如何正确捕捉/排除它?

解决方法

handle_sig是一个回调,因此它直接在事件循环之外运行,并且其异常只是通过全局挂钩报告给用户。如果希望在那里引发的异常在程序的其他地方被捕获,则需要使用将来的方法将异常从handle_sig转移到您希望其注意到的位置。

要在顶级捕获异常,您可能想引入另一种方法,我们将其称为async_main(),它等待 self.do_io()或先前创建的方法将来完成:

    def __init__(self):
        self.loop = asyncio.get_event_loop()
        self.done_future = self.loop.create_future()

    async def async_main(self):
        # wait for do_io or done_future,whatever happens first
        io_task = asyncio.create_task(self.do_io())
        await asyncio.wait([self.done_future,io_task],return_when=asyncio.FIRST_COMPLETED)
        if self.done_future.done():
            io_task.cancel()
            await self.done_future  # propagate the exception,if raised
        else:
            self.done_future.cancel()

要从handle_sig内部引发异常,只需在将来的对象上set the exception

    def handle_sig(self,signum):
        print(f"\npid: {os.getpid()},Received signal: {Signals(signum).name},raising error for exit")
        self.done_future.set_exception(ShutdownApp("Exiting"))

最后,您对run_app进行了修改,以将self.async_main()传递给run_until_complete,并且一切就绪:

$ python3 x.py
Starting Program
io start. Press Ctrl+C now.
^C
pid: 2069230,Received signal: SIGINT,raising error for exit
ShutdownApp caught: Exiting
Finished

最后,请注意,可靠捕获键盘中断是notoriously tricky undertaking,上面的代码可能无法涵盖所有​​极端情况。

,

如果我从下面的do_io()中抛出ShutdownApp,则可以在run_app()中正确捕获它。但是,当从handle_sig()引发异常时,我似乎无法捕捉到它。

响应以上给出的查询

自定义例外实现:

class RecipeNotValidError(Exception):    
    def __init__(self):       
        self.message = "Your recipe is not valid"        
        try:            
            raise RecipeNotValidError
        except RecipeNotValidError as e:            
            print(e.message)

在handle_sig方法中,添加try和except块。另外,您可以在自定义例外中自定义消息。

 def handle_sig(self,signum):
        try:
            print(f"\npid: {os.getpid()},raising 
                 error for exit")
            raise ShutdownApp("Exiting")
        except ShutdownApp as e:
            print(e.message)   

针对您的第二个查询: 是否可以从asyncio中的信号处理程序引发自定义异常?如果是这样,我如何正确捕捉/排除它?

Asyncio中的内置错误处理。有关更多文档,请访问https://docs.python.org/3/library/asyncio-exceptions.html


import asyncio
async def f():
    try:
        while True: await asyncio.sleep(0)
    except asyncio.CancelledError:  
        print('I was cancelled!')  
    else:
        return 111

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...