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

有点意思的 Python 系列二 内置类型增加额外方法 中介绍了一种通过 Python 代码实现对内置的类型增加自定义的方法。
今天再介绍一种方法,实现更为底层。那就是直接修改 cPython 的源码。
这里演示给 listdict 增加 deepcopytojson 方法,实现对 listdict 的深拷贝和把一个 listdict 转换为 json 的方法。
intfloat 增加 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 sha0181aa2e3efedc6504b27f6fe74f096e5e454286

修改 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_tojsontojson 的具体实现。
然后将上面两个方法增加到 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