有点意思的 Python 系列二 内置类型增加额外方法(二)

有点意思的 Python 系列二 内置类型增加额外方法 中介绍了一种通过 Python 代码实现对内置的类型增加自定义的方法。
今天再介绍一种方法,实现更为底层。那就是直接修改 cPython 的源码。
这里演示给 list 和 dict 增加 deepcopy 和 tojson 方法,实现对 list 和 dict 的深拷贝和把一个 list 和 dict 转换为 json 的方法。
给 int 和 float 增加 add, sub, mul, div 方法,实现加减乘除。

Step 1 下载代码

cPython 代码可以在 GitHub 上找到: cPython

配置环境

我使用的是 macOS 系统,所以这里就只介绍 macOS 需要的环境

  1. 安装 C 编译器和工具包
    xcode-select --install
  2. 安装其他工具
    brew install openssl@3 xz zlib gdbm sqlite

Step 2 修改代码

首先将分支切换到 3.12,不使用 main 的原因是 main 分支随时都在更新,所以选择旧版本的分支,确保代码的一致性。
确保 3.12 分支的代码 commit sha 为 0181aa2e3efedc6504b27f6fe74f096e5e454286

修改 list

list 的实现在 ./Objects/listobject.c 中。我们的修改就在这里。
首先找到 static PyMethodDef list_methods[] 这里包括了 list 相关的方法。在这一行的上面,增加以下代码

static PyObject *
list_method_deepcopy(PyObject *self, PyObject *args)
{
PyObject *copy_module = PyImport_ImportModule("copy");
if (copy_module == NULL) {
return NULL;
}

PyObject *deepcopy_func = PyObject_GetAttrString(copy_module, "deepcopy");
Py_DECREF(copy_module);
if (deepcopy_func == NULL) {
return NULL;
}

PyObject *result = PyObject_CallFunctionObjArgs(deepcopy_func, self, NULL);

Py_DECREF(deepcopy_func);

return result;
}

static PyObject *
list_method_tojson(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyObject *json_module = NULL;
PyObject *dumps_func = NULL;
PyObject *result = NULL;
PyObject *args_tuple = NULL;
PyObject *args_with_self = NULL;

json_module = PyImport_ImportModule("json");
if (json_module == NULL) {
return NULL;
}

dumps_func = PyObject_GetAttrString(json_module, "dumps");
Py_DECREF(json_module);
if (dumps_func == NULL) {
return NULL;
}

args_tuple = PyTuple_Pack(1, self);
Py_DECREF(dumps_func);
if (args_tuple == NULL) {
return NULL;
}

args_with_self = PySequence_Concat(args_tuple, args);
Py_DECREF(args_tuple);
if (args_with_self == NULL) {
return NULL;
}

result = PyObject_Call(dumps_func, args_with_self, kwargs);

Py_DECREF(args_with_self);

return result;
}

其中 list_method_deepcopy 就是 deepcopy 的具体实现,list_method_tojson 是 tojson 的具体实现。
然后将上面两个方法增加到 list_methods 中
注意,要增加到最后一行 {NULL, NULL} /* sentinel */ 上方

{"deepcopy", (PyCFunction)list_method_deepcopy, METH_NOARGS, PyDoc_STR("Return a deep copy of the list")},
{"tojson", (PyCFunction)list_method_tojson, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("Convert list to JSON string")},

最后完整的 list_methods[]

static PyMethodDef list_methods[] = {
{"__getitem__", (PyCFunction)list_subscript, METH_O|METH_COEXIST,
PyDoc_STR("__getitem__($self, index, /)\n--\n\nReturn self[index].")},
LIST___REVERSED___METHODDEF
LIST___SIZEOF___METHODDEF
LIST_CLEAR_METHODDEF
LIST_COPY_METHODDEF
LIST_APPEND_METHODDEF
LIST_INSERT_METHODDEF
LIST_EXTEND_METHODDEF
LIST_POP_METHODDEF
LIST_REMOVE_METHODDEF
LIST_INDEX_METHODDEF
LIST_COUNT_METHODDEF
LIST_REVERSE_METHODDEF
LIST_SORT_METHODDEF
{"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
{"deepcopy", (PyCFunction)list_method_deepcopy, METH_NOARGS, PyDoc_STR("Return a deep copy of the list")}, # 给 list 增加 deepcopy 方法,调用 list_method_deepcopy 实现
{"tojson", (PyCFunction)list_method_tojson, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("Convert list to JSON string")}, # 给 list 增加 tojson 方法,调用 list_method_tojson 实现
{NULL, NULL} /* sentinel */
};

修改 dict

和修改 list 类似,dict 的实现在 ./Objects/dictobject.c 中。
同理还是首先找到 static PyMethodDef mapp_methods[] 这里包括了 dict 相关的方法。在这一行的上面,增加以下代码。

static PyObject *
dict_method_deepcopy(PyObject *self, PyObject *args)
{
PyObject *copy_module = PyImport_ImportModule("copy");
if (copy_module == NULL) {
return NULL;
}
PyObject *deepcopy_func = PyObject_GetAttrString(copy_module, "deepcopy");
Py_DECREF(copy_module);
if (deepcopy_func == NULL) {
return NULL;
}
PyObject *result = PyObject_CallFunctionObjArgs(deepcopy_func, self, NULL);
Py_DECREF(deepcopy_func);
return result;
}

static PyObject *
dict_method_tojson(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyObject *json_module = NULL;
PyObject *dumps_func = NULL;
PyObject *result = NULL;
PyObject *args_tuple = NULL;
PyObject *args_with_self = NULL;

json_module = PyImport_ImportModule("json");
if (json_module == NULL) {
return NULL;
}

dumps_func = PyObject_GetAttrString(json_module, "dumps");
Py_DECREF(json_module);
if (dumps_func == NULL) {
return NULL;
}

args_tuple = PyTuple_Pack(1, self);
Py_DECREF(dumps_func);
if (args_tuple == NULL) {
return NULL;
}

args_with_self = PySequence_Concat(args_tuple, args);
Py_DECREF(args_tuple);
if (args_with_self == NULL) {
return NULL;
}

result = PyObject_Call(dumps_func, args_with_self, kwargs);

Py_DECREF(args_with_self);

return result;
}

可以看到,这里的代码除了方法名字,实现方法和 list 的一致,当然。最后也还是要加到 mapp_methods[] 中

{"deepcopy", (PyCFunction)dict_method_deepcopy, METH_NOARGS, PyDoc_STR("Return a deep copy of the dict")},
{"tojson", (PyCFunction)dict_method_tojson, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("Convert dict to JSON string")},

完整的 mapp_methods

static PyMethodDef mapp_methods[] = {
DICT___CONTAINS___METHODDEF
{"__getitem__", _PyCFunction_CAST(dict_subscript), METH_O | METH_COEXIST,
getitem__doc__},
{"__sizeof__", _PyCFunction_CAST(dict_sizeof), METH_NOARGS,
sizeof__doc__},
DICT_GET_METHODDEF
DICT_SETDEFAULT_METHODDEF
DICT_POP_METHODDEF
DICT_POPITEM_METHODDEF
{"keys", dictkeys_new, METH_NOARGS,
keys__doc__},
{"items", dictitems_new, METH_NOARGS,
items__doc__},
{"values", dictvalues_new, METH_NOARGS,
values__doc__},
{"update", _PyCFunction_CAST(dict_update), METH_VARARGS | METH_KEYWORDS,
update__doc__},
DICT_FROMKEYS_METHODDEF
{"clear", (PyCFunction)dict_clear, METH_NOARGS,
clear__doc__},
{"copy", (PyCFunction)dict_copy, METH_NOARGS,
copy__doc__},
DICT___REVERSED___METHODDEF
{"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
{"deepcopy", (PyCFunction)dict_method_deepcopy, METH_NOARGS, PyDoc_STR("Return a deep copy of the dict")}, # 给 dict 增加 deepcopy 方法,调用 dict_method_deepcopy 实现
{"tojson", (PyCFunction)dict_method_tojson, METH_VARARGS | METH_KEYWORDS, PyDoc_STR("Convert dict to JSON string")}, # 给 dict 增加 tojson 方法,调用 dict_method_tojson 实现
{NULL, NULL} /* sentinel */
};

修改 float

float 的实现在 ./Objects/floatobject.c 中。 同理找到 float_methods[] 在上方增加以下方法

static PyObject *
float_method_add(PyObject *self, PyObject *args)
{
PyObject *other;

if (!PyArg_ParseTuple(args, "O", &other))
return NULL;

if (PyFloat_Check(other) || PyLong_Check(other)) {
return PyNumber_Add(self, other);
}

PyErr_SetString(PyExc_TypeError, "Argument must be of type float or int");
return NULL;
}

static PyObject *
float_method_sub(PyObject *self, PyObject *args)
{
PyObject *other;

if (!PyArg_ParseTuple(args, "O", &other))
return NULL;

if (PyFloat_Check(other) || PyLong_Check(other)) {
return PyNumber_Subtract(self, other);
}

PyErr_SetString(PyExc_TypeError, "Argument must be of type float or int");
return NULL;
}

static PyObject *
float_method_mul(PyObject *self, PyObject *args)
{
PyObject *other;

if (!PyArg_ParseTuple(args, "O", &other))
return NULL;

if (PyFloat_Check(other) || PyLong_Check(other)) {
return PyNumber_Multiply(self, other);
}

PyErr_SetString(PyExc_TypeError, "Argument must be of type float or int");
return NULL;
}

static PyObject *
float_method_div(PyObject *self, PyObject *args)
{
PyObject *other;

if (!PyArg_ParseTuple(args, "O", &other))
return NULL;

if (PyFloat_Check(other) || PyLong_Check(other)) {
return PyNumber_TrueDivide(self, other);
}

PyErr_SetString(PyExc_TypeError, "Argument must be of type float or int");
return NULL;
}

类似的在 float_methods[] 中增加

{"add", (PyCFunction)float_method_add, METH_VARARGS, PyDoc_STR("Add float or int objects")},
{"sub", (PyCFunction)float_method_sub, METH_VARARGS, PyDoc_STR("Subtract float or int objects")},
{"mul", (PyCFunction)float_method_mul, METH_VARARGS, PyDoc_STR("Multiply float or int objects")},
{"div", (PyCFunction)float_method_div, METH_VARARGS, PyDoc_STR("Divide float or int objects")},

修改 int

int 的实现是在 .Objects/longobject.c 中。找到 long_methods[] 在上方增加方法

static PyObject *
long_method_add(PyObject *self, PyObject *args)
{
PyObject *other;

if (!PyArg_ParseTuple(args, "O", &other))
return NULL;

if (PyLong_Check(other) || PyFloat_Check(other)) {
return PyNumber_Add(self, other);
}

PyErr_SetString(PyExc_TypeError, "Argument must be of type int or float");
return NULL;
}

static PyObject *
long_method_sub(PyObject *self, PyObject *args)
{
PyObject *other;

if (!PyArg_ParseTuple(args, "O", &other))
return NULL;

if (PyLong_Check(other) || PyFloat_Check(other)) {
return PyNumber_Subtract(self, other);
}

PyErr_SetString(PyExc_TypeError, "Argument must be of type int or float");
return NULL;
}

static PyObject *
long_method_mul(PyObject *self, PyObject *args)
{
PyObject *other;

if (!PyArg_ParseTuple(args, "O", &other))
return NULL;

if (PyLong_Check(other) || PyFloat_Check(other)) {
return PyNumber_Multiply(self, other);
}

PyErr_SetString(PyExc_TypeError, "Argument must be of type int or float");
return NULL;
}

static PyObject *
long_method_div(PyObject *self, PyObject *args)
{
PyObject *other;

if (!PyArg_ParseTuple(args, "O", &other))
return NULL;

if (PyLong_Check(other) || PyFloat_Check(other)) {
return PyNumber_TrueDivide(self, other);
}

PyErr_SetString(PyExc_TypeError, "Argument must be of type int or float");
return NULL;
}

这里的方法实现和 int 中的实现有细微差别。最后还是一样增加 long_methods[]

{"add", (PyCFunction)long_method_add, METH_VARARGS, PyDoc_STR("Add int or float objects")},
{"sub", (PyCFunction)long_method_sub, METH_VARARGS, PyDoc_STR("Subtract int or float objects")},
{"mul", (PyCFunction)long_method_mul, METH_VARARGS, PyDoc_STR("Multiply int or float objects")},
{"div", (PyCFunction)long_method_div, METH_VARARGS, PyDoc_STR("Divide int or float objects")},

Step 3 编译

使用以下命令编译代码

LDFLAGS="-L$(brew --prefix zlib)/lib -L$(brew --prefix bzip2)/lib -L$(brew --prefix openssl@3)/lib" \
CPPFLAGS="-I$(brew --prefix zlib)/include -I$(brew --prefix bzip2)/include -I$(brew --prefix openssl@3)/include" \
./configure --with-openssl=$(brew --prefix openssl@3) && make -j$(nproc) -s

编译成功后在目录下会有一个 python.exe (为什么在 macOS 是 .exe 可参考 build-instructions)

Step 4 测试

新建一个 .py 文件

origin_dict = {"goods": ["apple", "orange"]}
print('=' * 50)
print(f"原始字典: {origin_dict}")
copy_dict = origin_dict.copy() # 自带的 copy() 浅拷贝
deepcopy_dict = origin_dict.deepcopy() # 添加的 deepcopy() 深拷贝
print('=' * 50)
print(f".copy() 字典: {copy_dict}")
print(f".deepcopy() 字典: {deepcopy_dict}")
print('=' * 50)
origin_dict["goods"].append("banana")
print(f"原始字典修改: {origin_dict}")
print('=' * 50)
print(f".copy() 结果: {copy_dict}")
print(f".deepcopy() 结果: {deepcopy_dict}")
print('=' * 50)

int_number = 1
float_number = 2.15

print(int_number.add(2).add(3).div(2).mul(4)) # (1 + 2 + 3) / 2 * 4
print('=' * 50)
print(float_number.add(2).add(3).div(3).mul(7)) # (2.15 + 2 + 3) / 3 * 7

输出结果如下

==================================================
原始字典: {'goods': ['apple', 'orange']}
==================================================
.copy() 字典: {'goods': ['apple', 'orange']}
.deepcopy() 字典: {'goods': ['apple', 'orange']}
==================================================
原始字典修改: {'goods': ['apple', 'orange', 'banana']}
==================================================
.copy() 结果: {'goods': ['apple', 'orange', 'banana']}
.deepcopy() 结果: {'goods': ['apple', 'orange']}
==================================================
12.0
==================================================
16.683333333333334

由此完成了从 cPython 的改造实现了以上几种方法。

补充

  1. 直接修改 cPython 的源码实现好处是实现了原生实现。但是缺点也很明显。就是这样写的代码,只能运行在这个修改后的 Python 环境,否则就会报错。
    但是换个思路想,这样也会带来一个好处就是间接的保护了代码。这个具体的内容后面可以在展开聊聊。
  2. 这里编译的 Python 其实只是一个调试环境,因为没有启用 PGO 所以编译很快,但是不能用于生产环境。
    如果想把这个版本的 Python 用在生产环境,可以在 ./configure 的时候增加 --enable-optimizations 参数。具体内容可参考 profile-guided-optimization