擴展Python模塊系列(五)----異常和錯誤處理


在上一節中,討論了在用C語言擴展Python模塊時,應該如何處理無處不在的引用計數問題。重點關注的是在實現一個C Python的函數時,對於一個PyObject對象,何時調用Py_INCREF和Py_DECREF。在編寫C語言代碼時,需要了解Python提供的C/C++ API的實現細節,特別是有的API內部實現會調用Py_INCREF,這時自己編寫的函數可能需要調用Py_DECREF,而有的API內部實現只是borrowed reference,此時一般不應該調用Py_DECREF。

本節討論在C擴展Python時,如何對異常和錯誤進行處理。Python解釋器的實現中有一個重要的約定:當一個函數失敗,它應該設置一個exception condition並返回一個錯誤值(通常是NULl指針)。異常是存放在解釋器的一個全局變量中,如果這個變量是NULL,那么沒有異常發生。另外一個全局變量存放的是跟異常相關的值,還有一個變量包含了stack traceback,記錄了產生錯誤時的Python Code。這三個變量對應Python的sys模塊的三個變量,sys.exc_type, sys.exc_value, sys.exc_traceback。在1.5版本之后,這三個變量用exc_info()代替了。

這三個全局的變量在C Python 源碼中是存放在PyThreadState *_PyThreadState_Current這個結構體中的, 而_PyThreadState_Current是在pythonrun.c的Py_InitializeEx中初始化的。

 PyThreadState結構體定義:

紅色框中的三個變量就是error indicator。

常見的Python設置異常的接口

Python API定義了一系列的function來設置不同類型的異常,定義在Python源碼中的Python/error.c中。

PyErr_SetString:

最常用的莫過於PyErr_SetString,函數的原型為:

 函數的作用是設置Python解釋器的全局error indicator。 

參數分別為一個exception對象和一個描述異常的字符串。exception object通常是一個已經定義好的object, 不需要增加它的引用計數,在PyErr_SetString源碼中已經調用了Py_XINCREF(exception)。

/* Predefined exceptions */

PyAPI_DATA(PyObject *) PyExc_BaseException;
PyAPI_DATA(PyObject *) PyExc_Exception;
PyAPI_DATA(PyObject *) PyExc_StopIteration;
PyAPI_DATA(PyObject *) PyExc_GeneratorExit;
PyAPI_DATA(PyObject *) PyExc_StandardError;
PyAPI_DATA(PyObject *) PyExc_ArithmeticError;
PyAPI_DATA(PyObject *) PyExc_LookupError;

PyAPI_DATA(PyObject *) PyExc_AssertionError;
PyAPI_DATA(PyObject *) PyExc_AttributeError;
PyAPI_DATA(PyObject *) PyExc_EOFError;
PyAPI_DATA(PyObject *) PyExc_FloatingPointError;
PyAPI_DATA(PyObject *) PyExc_EnvironmentError;
PyAPI_DATA(PyObject *) PyExc_IOError;
PyAPI_DATA(PyObject *) PyExc_OSError;
PyAPI_DATA(PyObject *) PyExc_ImportError;
PyAPI_DATA(PyObject *) PyExc_IndexError;
PyAPI_DATA(PyObject *) PyExc_KeyError;
PyAPI_DATA(PyObject *) PyExc_KeyboardInterrupt;
PyAPI_DATA(PyObject *) PyExc_MemoryError;
PyAPI_DATA(PyObject *) PyExc_NameError;
PyAPI_DATA(PyObject *) PyExc_OverflowError;
PyAPI_DATA(PyObject *) PyExc_RuntimeError;
PyAPI_DATA(PyObject *) PyExc_NotImplementedError;
PyAPI_DATA(PyObject *) PyExc_SyntaxError;
PyAPI_DATA(PyObject *) PyExc_IndentationError;
PyAPI_DATA(PyObject *) PyExc_TabError;
PyAPI_DATA(PyObject *) PyExc_ReferenceError;
PyAPI_DATA(PyObject *) PyExc_SystemError;
PyAPI_DATA(PyObject *) PyExc_SystemExit;
PyAPI_DATA(PyObject *) PyExc_TypeError;
PyAPI_DATA(PyObject *) PyExc_UnboundLocalError;
PyAPI_DATA(PyObject *) PyExc_UnicodeError;
PyAPI_DATA(PyObject *) PyExc_UnicodeEncodeError;
PyAPI_DATA(PyObject *) PyExc_UnicodeDecodeError;
PyAPI_DATA(PyObject *) PyExc_UnicodeTranslateError;
PyAPI_DATA(PyObject *) PyExc_ValueError;
PyAPI_DATA(PyObject *) PyExc_ZeroDivisionError;

PyErr_SetObject:

從PyErr_SetString實現的源碼中可以看到,內部調用了PyErr_SetObject, PyErr_SetObject內部又調用了PyErr_Restore。

PyErr_SetObject函數原型是:

PyObject* PyErr_Occurred():

函數返回當前是否有異常發生,如果有返回current exception object【borrowed reference】,否則返回NULL

int PyErr_ExceptionMatches(PyObject* exc)

int PyErr_GivenExceptionMatches(PyObject* given, PyObject* exc):

判斷給定的異常對象是否符合exc類型。

PyErr_Clear():

清除Python解釋器的error indicator。Python解釋器不會檢測到有異常發生。

PyErr_Fetch(PyObject** ptype, PyObject** pvalue, PyObject** ptraceback)

獲得error indicator的三個變量,如果error indicator沒有設置,ptype, pvalue, ptraceback都被設置為NULL。error indicator會被置空,將三個變量的地址賦給ptype, pvalue, ptraceback。

PyErr_Restore(PyObject* type, PyObject* value, PyObject* traceback)

使用給定的三個變量設置error indicator。

 

代碼中使用異常接口的規則

 如果函數f調用函數g,其中g函數失敗,應該怎樣設置異常呢?通常的做法是在g中調用設置異常的各個接口,比如PyErr_SetString,通知Python解釋器有異常發生了,函數g然后返回一個NULL給函數f,而f不用再處理異常,因為函數g已經上報過了。比如我們自己寫的函數調用了PyArg_ParseTuple(),這個函數出現錯誤時返回NULL,但是我們不用自己上報異常,異常的上報由PyArg_ParseTuple本身處理。一旦通過PyErr_SetString設置了異常,那么Python解釋器在主循環中檢測到error indicator被設置,會暫停執行當前的Python Code,會試圖尋找exception handler來處理異常。

 

Python源碼中使用異常處理接口的例子

PyTuple_GetItem:

PyObject *
PyTuple_GetItem(register PyObject *op, register Py_ssize_t i)
{
    if (!PyTuple_Check(op)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    if (i < 0 || i >= Py_SIZE(op)) {
        // 如果索引有錯誤,設置異常
        PyErr_SetString(PyExc_IndexError, "tuple index out of range");
        return NULL;
    }
    return ((PyTupleObject *)op) -> ob_item[i];
}

PyErr_SetString實現:

void
PyErr_SetString(PyObject *exception, const char *string)
{
    PyObject *value = PyString_FromString(string);
    PyErr_SetObject(exception, value);
    Py_XDECREF(value);
}

PyErr_SetObject實現:

void
PyErr_SetObject(PyObject *exception, PyObject *value)
{
    Py_XINCREF(exception);  // 增加引用計數
    Py_XINCREF(value);   // 增加引用計數
    PyErr_Restore(exception, value, (PyObject *)NULL);
}

PyErr_Restore實現:

void
PyErr_Restore(PyObject *type, PyObject *value, PyObject *traceback)
{
    PyThreadState *tstate = PyThreadState_GET();
    PyObject *oldtype, *oldvalue, *oldtraceback;

    if (traceback != NULL && !PyTraceBack_Check(traceback)) {
        /* XXX Should never happen -- fatal error instead? */
        /* Well, it could be None. */
        Py_DECREF(traceback);
        traceback = NULL;
    }

    /* Save these in locals to safeguard against recursive
       invocation through Py_XDECREF */
    oldtype = tstate->curexc_type;
    oldvalue = tstate->curexc_value;
    oldtraceback = tstate->curexc_traceback;
    // 設置當前的異常
    tstate->curexc_type = type;
    tstate->curexc_value = value;
    tstate->curexc_traceback = traceback;
    
    // 舊的異常變量需要減少引用計數
    Py_XDECREF(oldtype);
    Py_XDECREF(oldvalue);
    Py_XDECREF(oldtraceback);
}

 

自定義異常

 除了使用Python已經定義好的異常對象之外,我們可以自定義異常類型,主要是通過PyErr_NewException創建一個異常對象。

1. 在文件頭部定義一個static 變量:

  static PyObject *MyError;

2. 在模塊初始化時傳入我們自定義的異常對象

PyMODINIT_FUNC
inittest(void)
{
    PyObject *m;

    m = Py_InitModule("test", TestMethods);
    if (m == NULL)
        return;

    MyError = PyErr_NewException("test.error", NULL, NULL);
    Py_INCREF(MyError);
    PyModule_AddObject(m, "error",MyError);
}

3. 在通過PyErr_SetString設置異常時,第一個參數傳入MyError即可。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM