呱呱老师 阅读(29) 评论(0)

异常:对程序运行中的非正常情况进行抽象。并且提供相应的语法结构和语义元素,使得程序员能够通过这些语法结构和语义元素来方便地描述异常发生时的行为。

1.Python中的异常机制:

1.1Python虚拟机自身抛出异常

python虚拟机内部本身有一套异常捕捉机制,即使python中没有出现try、except、finally等用于进行异常控制的语义元素,Python脚本执行中所抛出的异常还是会被python虚拟机捕捉到。

比如1/0肯定会抛出异常。

异常是在执行除法操作字节码的时候被触发的,

执行除法字节码在运行时栈中获取两个数据,进行除操作,并且将结果压栈,然后进行有效性检查,如果有效,就继续下一条字节码;如果无效,就break跳出python虚拟机中的那个对字节码进行分发的switch语句。

异常就是在Pyumber_Divide执行时抛出的,而且异常的情况返回值为NULL,从而导致python虚拟机退出当前栈帧。

问题来了:异常从哪里抛出的,又抛到哪里去了呢?

这就要深入Pynumber_Divide了。

在Pynumber_Divide中,不同参数走向不同的路,int对象就会走向PyInt_Type中定义的int_classic_div中,而int_classic_div会调用i_divmod,在i_divmod中抛出了异常,并且返回了一个不OK或者溢出的标志,这样,int_classic_div才能返回一个表示除法失败的NULL值,从而将Python虚拟机扼杀在执行的道路上。

揭开美女的最后一层面纱,我们发现在i_divmod中,会去判断除数是否是0,如果是0,就调用PyErr_SetString抛出异常。并返回异常标志码——DIVMOD_ERROR.

在python中,异常也是对象,在python运行环境初始化的时候,各种异常都会被初始化。

现在知道异常从哪里抛出了。

 

1.2在i_divmod中的抛出异常函数PyErr_SetString中,会依次调用PyErr_SetObject->PyErr_Restore.最终会将异常放到一个安全的地方,不会被坏人发现。

但是tianwanghuihuishuerbulou,我只要默默的跟踪这你,就能够顺藤摸瓜,找到你究竟把异常放到哪里去了。

所以我去看了PyErr_Restore函数中,

我发现,获取了当前线程对象tstate,并且将异常放到了线程对象的成员中了,tstate->curexc_type = type; tstate->curexc_value = value; tstate->curexc_traceback = traceback;然后抛弃以前的异常。

这个线程就是操作系统提供给python运行的线程。真实的线程由操作系统来管理,我们不用管他。

但是,python虚拟机在运行时需要另外一些与线程相关的状态和信息,比如是否发生了异常等。这些信息操作系统肯定管不着。所以Python就为线程准备了一个在python虚拟机一级的对象,该对象专门用于保存线程状态信息。

获取这个对象的接口就是Pyhreadtate_GET。

我们看到在代码中就是先获取线程对象,然后存储异常信息的。这样之后,异常就和线程关联起来了。

当异常发生时,我们可以在python中可以通过sys.exc_info()来获取异常。

 

1.3展开栈帧

异常之后就会跳出字节码分派的switch,之后会立马检查x的值,即检查除法的结构,如果x为NULL,说明有异常,那么就把设置一个变量why=WHY_EXCEPTION,而这个why实际上就是维护python虚拟机中执行字节码的那个for循环的状态,

到这里,设置了why之后,python虚拟机才开始获得信息,即获得执行过程中发生的异常。

python通过检查why,意识到有异常了,

这时候才会进一步进入到异常处理流程。

在异常处理的流程中,有一个traceback对象,这个对象记录栈帧链表的信息,利用这个对象将链表中的每一个栈帧的当前状态可视化。从而输出我们常见的信息。

 

python处理异常时,首先创建一个traceback对象。用于记录异常发生时活动栈帧的状态。

创建时,先获取线程现有的traceback对象,然后把新建的traceback的last指向旧的。并且将新建的traceback设置到线程中去。

需要知道的是,这个traceback其实也是一个链表形式,一个traceback和一个frameobject对应。

traceback的结构体中包括了*tb_next, *tb_frame, tb_lineno,tb_lasti等字段。

 

虚拟机意识到有异常,并且创建了traceback对象,然后,它会从当前帧中找except语句,以寻找开发人员指定的捕获异常动作,如果没找到,那么退出当前活动栈帧,并且沿着当前栈帧链回退到上一个栈帧。

这个回退的动作在PyEval_EvalFrameEx的最后完成。

如果开发人员没有提供任何捕获异常的动作,那么程序将结束for循环。同时重新设置当前线程状态对象中活动栈帧,完成栈帧回退的动作。

PyEval_EvalFrameEx到此结束,请各位有序退场。。。。

等一下,别走,PyEval_EvalFrameEx返回到哪里去了呢?

哈哈,答案是返回到PyEval_EvalFrameEx中去了。

为什么?你tm的满嘴跑火车,

其实python虚拟机中这个函数是会递归调用的。从名字上也可以看出,她是与某个PyFrameObject对象执行有关的函数,所以用递归模拟PyFrameObject的链表。

新创建PyFrameObject的时候,同时会递归调用PyEval_EvalFrameEx.

 

最后说两句,

异常发生后,PyEval_EvalFrameEx返回的是NULL,所以退回到上一层也会意识到有异常发生,所以同样会创建traceback,同样寻找except,如果没有捕获异常动作,就继续回退。

这种沿着栈帧不断回退的过程我们叫栈帧展开。展开过程中,不断创建与各个栈帧对应的traceback对象,并将其链接成链表。

如果一直都没有捕获,那么最后就会返回到PyRun_SimpleFileExFlags中。

为什么?别问我为什么,书上还没说,以后可能会说。

在PyRun_SimpleFileExFlags中,如果发现PyEval_EvalFrameEx的返回值是NULL,就会调用PyErr_Print(),这个函数就会从线程中取出traceback对象,并且遍历traceback对象链表,逐个输出其中信息,最终打印出我们平时看到的信息。

 

 

2.Python中的异常控制语义结构

在PyFrameObject的f_blockstack中拿两块用于捕捉异常,

抛出异常字节码先创建一个异常对象,然后压入运行时栈,

然后把这个对象取出,然后调用do_raise,do_raise最终调用Py_Restore将异常对象存储到当前线程状态对象中。在do_raise的最后,返回了一个WHY_EXCEPTION,这就是why的最终状态。然后python虚拟机跳出switch语句,结束字节码的分发。

开始了异常捕获的动作。

在经过一系列动作之后(设置traceback等)。python虚拟机携带着(why=WHY_EXCEPTION, f_iblock=2)的信息抵达真正捕获异常的代码。

首先从PyFrameObject对象中的f_blockstack中弹出PyTryBlock,获得异常信息, 另一方面PyErr_Fetch得到当前线程状态对象中存储的最新的异常对象和traceback对象。然后将一场信息压栈,并且设置why = WHY_NOT;因为虚拟机已经捕获到异常了,自己的状态可以还原了。

接下来就是处理异常。

先比较捕获到的异常和except表达式中指定的异常是否匹配。

如果不匹配,会设置why为有问题,然后展开栈帧。

最后给个相信流程图:

如果匹配,就会取出异常信息,然后处理掉。、