《Python源码剖析》读书笔记-10 Python虚拟机中的控制流


第10章 Python虚拟机中的控制流

  • if条件判断语句,在虚拟机中对应了COMPARE_OP,其中有几个地方需要理解:

    • 在slow_compare中的cmp_outcome函数,处理了Python自定义的一些比较操作,例如is,is not,in等。
    • 对于那些一般类型的比较操作(非数字,非is,is not这类Python特殊比较符),会通过PyObject_RichCompare来进行。这个函数里面的核心任务,是把待比较对象的类型对象(ob_type)上定义的比较函数指针(tp_richcompare或者tp_compare,优先tp_richcompare)取出,用这个比较函数来进行比较。如果这两个函数都不能进行比较,虚拟机还会调用更普适但是也更慢的do_richcmp。
    • 比较操作最后返回的结果,是两个对象,Py_True和Py_False。这两个对象其实都是PyIntObject类型的,包装了1和0。
    • 位于比较操作最后的PREDICT宏,用于预测下一条指令,如果预测正确则可以跳过许多不必要的步骤,直接执行下一条指令。这种做法的基础是,在Python源码编译出的字节码中,有一些指令经常是成对出现的,例如COMPARE_OP 和 POP_JUMP_IF_FALSE&POP_JUMP_IF_TRUE。所以在处理COMPARE_OP的最后,可以添加如下预测语句。

      1
      2
      PREDICT(POP_JUMP_IF_FALSE);
      PREDICT(POP_JUMP_IF_TRUE);
    • PREDICT宏的定义为:

      1
      #define PREDICT(op) if (*next_instr == op) goto PRED_##op
    • 正好就是判断下一条字节码,预测成功就直接执行下一条字节码的节奏。

  • for循环语句,通过以下字节码指令完成

    • SETUP_LOOP。在当前栈帧的f_blockstack数组中,取出了一块新的PyTryBlock,保存了部分信息。

      1
      2
      3
      4
      5
      typedef struct {
      int b_type; /* what kind of block this is */
      int b_handler; /* where to jump to find handler */
      int b_level; /* value stack level to pop to */
      } PyTryBlock;
    • 这里b_type存在的意义是因为不止一个字节码指令使用了PyTryBlock信息,所以要用它加以区分;b_handler与异常处理有关;b_level存储了在SETUP_LOOP之前,程序的运行时栈保存了多少数据,用于在程序退出这块Block(这里是for循环)时恢复运行时栈原来的样子。

    • GET_ITER用于获取一个list上的迭代器对象(这里的for循环是在一个list上循环)
    • FOR_ITER使用迭代器获取list上的下一个元素,同时如果没有下一个元素了,就直接跳到POP_BLOCK
    • JUMP_ABSOLUTE在循环内字节码执行完毕后,强制使得程序跳回FOR_ITER
    • POP_BLOCK用于获得在SETUP_LOOP中存储的PyTryBlock,利用其中的信息恢复运行时栈原来的样子。
  • while循环语句,与for没什么差别。这部分讲解了continue和break语句的实现

    • continue很简单,会被编译成一些清理运行时栈的字节码指令(如POP),以及JUMP_ABSOLUTE,用于使虚拟机直接开始下一个循环。
    • break相对复杂一些,首先它会将一个指示循环结束原因的标识WHY_BREAK放入变量why,然后使用一个goto,跳出那个巨大的switch…case…语句,这里同样会通过PyTryBlock回复运行时栈,why置为WHY_NOT,表示没有异常发生,并且利用PyTryBlock中的b_handler属性,将虚拟机当前执行的指令指向循环后的语句,完成break的功能。
  • 异常机制

    • 栈帧展开

      • 执行字节码指令出现异常时,会在出现异常的地方为PyExc_ZeroDivisionError赋值说明异常原因(这里举的例子是1/0这样的一个异常,所以使用了PyExc_ZeroDivisionError,其实,在源码中还定义了许多其他异常,如PyExc_MemoryError等,这些异常都是PyObject*类型的),然后将返回值x设为NULL,跳出那个巨大的switch…case语句。
      • [4.1.2]在switch…case语句之后,检查x是否为NULL,如果是则将why设置为WHY_EXCEPTION
      • 之后进入关键的PyTraceBack_Here函数,取得当前的线程状态对象tstate,在tstate中存在一个curexc_traceback对象,其类型为PyTracebackObject。

        1
        2
        3
        4
        5
        6
        7
        typedef struct _traceback {
        PyObject_HEAD
        struct _traceback *tb_next;
        struct _frame *tb_frame;
        int tb_lasti;
        int tb_lineno;
        } PyTracebackObject;
      • 可以看出PyTracebackObject实际上是一个链表结构,并且其中保存了当前的栈帧对象tb_frame

      • 所以在PyTraceBack_Here中,最主要的工作就是使用当前栈帧对象新建了一个PyTracebackObject的节点,并加入tstate的curexc_traceback链表中。
      • [4.1.6]之后设置tstate的frame(当前栈帧对象)为f->f_back,即设置当前栈帧为上一个栈帧,并以NULL值从PyEval_EvalFrameEx中返回。这一步就返回到了调用此出现异常的函数的那个栈帧中,再从4.1.2开始设置一遍PyTracebackObject对象。这样一步步的回退栈帧,就在tstate对象中保存了异常出现时的栈帧调用次序。
      • 最后,如果没有异常捕获代码的话,程序返回到PyRun_SimpleFileExFlags中,调用PyErr_Print获得前面设置好的PyTracebackObject的链表,打印出异常信息。
    • 异常捕获
      • 如果存在try…except…finally..这样的异常捕获代码,那么首先在进入try代码块时,会执行SETUP_FINALLY和SETUP_EXCEPT两条语句,这两条语句和SETUP_LOOP一样,各自新建了一个PyTryBlock,存放了关于handler、type的信息。
      • 异常发生后,与前述步骤一样,会跳出那个巨大的switch…case语句,然后,调用PyTraceBack_Here函数。
      • 如果当前栈帧中没有异常捕获代码,那么会按照上述4.16的步骤,返回上一个栈帧的PyEval_EvalFrameEx函数中。
      • 如果当前栈帧中有try..except..finally…这样的异常捕获代码,那么一定在当前栈帧中保存了若干个PyTryBlock对象,并且其type是SETUP_FINALLY或者SETUP_EXCEPT。
      • 那么,首先弹出SETUP_EXCEPT类型的PyTryBlock,并从tstate中获得异常的类型、值、和traceback对象,压入运行时栈中;然后根据PyTryBlock中handler的值跳转字节码的地址。并将why设为WHY_NOT,表示已经找到了异常捕获代码,可以处理该异常。
      • 跳转到的字节码,会比较运行时栈内的异常类型和源代码编译出来的异常类型是否匹配。如果匹配,就依次执行异常捕获代码except代码块里的字节码;如果不匹配,那么把之前放入运行时栈的异常信息取出,重新构造异常抛出。
      • 无论异常是否匹配,最后都会执行到POP_BLOCK字节码指令。它会将代表SETUP_FINALLY的那个PyTryBlock取出,然后开始执行finally代码块中的字节码。

欢迎关注我的微信公众号,技术·生活·思考:
后端技术小黑屋

Comments