深入理解 Python 虛擬機器:複數(complex)的實現原理及原始碼剖析

一無是處的研究僧發表於2023-03-14

深入理解 Python 虛擬機器:複數(complex)的實現原理及原始碼剖析

在本篇文章當中主要給大家介紹在 cpython 虛擬機器當中是如何實現 複數 complex 這個資料型別的,這個資料型別在 cpython 當中一應該是一個算比較簡單的資料型別了,非常容易理解。

複數資料結構

在 cpython 當中對於複數的資料結構實現如下所示:

typedef struct {
    double real;
    double imag;
} Py_complex;
#define PyObject_HEAD                   PyObject ob_base;
typedef struct {
    PyObject_HEAD
    Py_complex cval;
} PyComplexObject;
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

上面的資料結構圖示如下:

複數的資料在整個 cpython 虛擬機器當中來說應該算是比較簡單的了,除了一個 PyObject 頭部之外就是實部和虛部了。

  • ob_refcnt,表示物件的引用記數的個數,這個對於垃圾回收很有用處,後面我們分析虛擬機器中垃圾回收部分在深入分析。

  • ob_type,表示這個物件的資料型別是什麼,在 python 當中有時候需要對資料的資料型別進行判斷比如 isinstance, type 這兩個關鍵字就會使用到這個欄位。

  • real,表示複數的實部。

  • imag,表示複數的虛部。

複數的操作

複數加法

下面是 cpython 當中對於複數加法的實現,為了簡潔刪除了部分無用程式碼。

static PyObject *
complex_add(PyObject *v, PyObject *w)
{
    Py_complex result;
    Py_complex a, b;
    TO_COMPLEX(v, a); // TO_COMPLEX 這個宏的作用就是將一個 PyComplexObject 中的 Py_complex 物件儲存到 a 當中
    TO_COMPLEX(w, b);
    result = _Py_c_sum(a, b); // 這個函式的具體實現在下方
    return PyComplex_FromCComplex(result); // 這個函式的具體實現在下方
}

// 真正實現複數加法的函式
Py_complex
_Py_c_sum(Py_complex a, Py_complex b)
{
    Py_complex r;
    r.real = a.real + b.real;
    r.imag = a.imag + b.imag;
    return r;
}

PyObject *
PyComplex_FromCComplex(Py_complex cval)
{
    PyComplexObject *op;

    /* Inline PyObject_New */
    // 申請記憶體空間
    op = (PyComplexObject *) PyObject_MALLOC(sizeof(PyComplexObject));
    if (op == NULL)
        return PyErr_NoMemory();
    // 將這個物件的引用計數設定成 1
    (void)PyObject_INIT(op, &PyComplex_Type);
    // 將複數結構體儲存下來
    op->cval = cval;
    return (PyObject *) op;
}

上面程式碼的整體過程比較簡單:

  • 首先先從 PyComplexObject 提取真正的複數部分。
  • 將提取到的兩個複數進行相加操作。
  • 根據得到的結果在建立一個 PyComplexObject 物件,並且將這個物件返回。

複數取反

複數取反操作就是將實部和虛部取相反數就可以了,這個操作也比較簡單。

static PyObject *
complex_neg(PyComplexObject *v)
{
    Py_complex neg;
    neg.real = -v->cval.real;
    neg.imag = -v->cval.imag;
    return PyComplex_FromCComplex(neg);
}

PyObject *
PyComplex_FromCComplex(Py_complex cval)
{
    PyComplexObject *op;

    /* Inline PyObject_New */
    op = (PyComplexObject *) PyObject_MALLOC(sizeof(PyComplexObject));
    if (op == NULL)
        return PyErr_NoMemory();
    (void)PyObject_INIT(op, &PyComplex_Type);
    op->cval = cval;
    return (PyObject *) op;
}

Repr 函式

我們現在來介紹一下一個有趣的方法,就是複數型別的 repr 函式,這個和類的 __repr__ 函式是作用是一樣的我們看一下複數的輸出是什麼:

>>> data = complex(0, 1)
>>> data
1j
>>> data = complex(1, 1)
>>> data
(1+1j)
>>> print(data)
(1+1j)

複數的 repr 對應的 C 函式如下所示:

static PyObject *
complex_repr(PyComplexObject *v)
{
    int precision = 0;
    char format_code = 'r';
    PyObject *result = NULL;

    /* If these are non-NULL, they'll need to be freed. */
    char *pre = NULL;
    char *im = NULL;

    /* These do not need to be freed. re is either an alias
       for pre or a pointer to a constant.  lead and tail
       are pointers to constants. */
    char *re = NULL;
    char *lead = "";
    char *tail = "";
    // 對應實部等於 0 虛部大於 0 的情況
    if (v->cval.real == 0. && copysign(1.0, v->cval.real)==1.0) {
        /* Real part is +0: just output the imaginary part and do not
           include parens. */
        re = "";
        im = PyOS_double_to_string(v->cval.imag, format_code,
                                   precision, 0, NULL);
        if (!im) {
            PyErr_NoMemory();
            goto done;
        }
    } else {
        /* Format imaginary part with sign, real part without. Include
           parens in the result. */
        // 將實部浮點數變成字串
        pre = PyOS_double_to_string(v->cval.real, format_code,
                                    precision, 0, NULL);
        if (!pre) {
            PyErr_NoMemory();
            goto done;
        }
        re = pre;
        // 將虛部浮點數變成字串
        im = PyOS_double_to_string(v->cval.imag, format_code,
                                   precision, Py_DTSF_SIGN, NULL);
        if (!im) {
            PyErr_NoMemory();
            goto done;
        }
        // 用什麼括號包圍起來
        lead = "(";
        tail = ")";
    }
    result = PyUnicode_FromFormat("%s%s%sj%s", lead, re, im, tail);
  done:
    PyMem_Free(im);
    PyMem_Free(pre);

    return result;
}

我們現在修改源程式將上面的 () 兩個括號變成 [],編譯之後執行的結果如下所示:

可以看到括號變成了 [] 。

總結

在本篇文章當中主要給大家介紹了在 cpython 虛擬機器當中對於複數這一型別的資料結構以及他的具體實現。總體來說這個資料結構比較簡單,操作也相對容易,比較容易理解,最後簡單介紹了一下複數型別的 repr 實現,其實這個函式和 python 的型別系統有關,目前我們還沒有仔細去討論這一點,在後續的文章當中我們將深入的去學習這個知識點,現在我們就先了解其中部分函式即可。


本篇文章是深入理解 python 虛擬機器系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演算法與資料結構)知識。

相關文章