Skip to content

字典遍历在 Python 中十分的常见,那么在 C/C++ 开发扩展的过程中,如何实现字典的遍历呢?接下来的实验会让我们更加清晰的了解字典遍历的底层开发。

编写程序遍历字典

我们会在 C/C++ 开发的扩展中实现一个遍历字典的函数,函数的参数为字典,无返回值。程序中重要的部分我都标注了注释,方便大家阅读。

开发过程依旧是创建一个函数,并将函数绑定到 demoModule 模块上。函数内部实现相当清晰,首先是通过 PyMapping_Items 获取字典,如果结果为 NULL,则直接返回空,这里参数一定要注意一下,如果传入的参数不为 hash 结构,如传入的参数为 list 时,程序会抛出如下错误 AttributeError: 'list' object has no attribute 'items'。接下来就是获取结构为元祖(key, value)的数组长度,循环遍历,得到对应所有的值。当然我们也可以使用 PyArg_ParseTuple 解析元祖,s 表示字符串,i 表示一个 int 类型的数值,依次输出结果。

如下程序不够严谨,没有对参数做严格的数据类型校验。其实 PyArg_ParseTuple 的第二个参数我们可以稍做调整就可以对参数类型做数据校验了,这里将 O 替换为 O!,并通过第三个参数限定数据类型为字典类型 PyDict_Type,如果不是字典类型,程序会抛出 TypeError 的异常,TypeError: argument 1 must be dict, not list

将一个 Python 对象存入一个 C 指针。和 O 类似,但是需要两个 C 参数:第一个是 Python 类型对象的地址,第二个是存储对象指针的 C 变量 ( PyObject* 变量) 的地址。如果 Python 对象类型不对,会抛出 TypeError 异常。

c
// hash_print.cpp
#include "Python.h"
#include <stdio.h>

static PyObject *hash_print(PyObject *self, PyObject *args) {
    PyObject *map_item;
    // if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &map_item)) {
    if (!PyArg_ParseTuple(args, "O", &map_item)) {
        return NULL;
    }
    // 将字典解析为一个元祖为 (key, value) 的数组
    PyObject *items = PyMapping_Items(map_item);
    if (items == NULL) {
        return NULL;
    }
    PyObject *item = NULL;
    // 获取字典的长度
    int l = PyMapping_Length(items);
    const char *key;
    int value;
    for (int i = 0; i < l; i++) {
        // 从数组中索引元素
        item = PySequence_GetItem(items, i);
        // 打印元素
        PyObject_Print(item, stdout, Py_PRINT_RAW);
        // 解析元祖,s 表示字符串,i 表示一个 int 类型的数值
        if (!PyArg_ParseTuple(item, "si", &key, &value)){
            return nullptr;
        }
        printf("\nkey: %s, value: %d\n", key, value);
        Py_DECREF(item);
    }
    Py_DECREF(items);
    return Py_BuildValue("");
}


static PyMethodDef MyDemoMethods[] = {
        {"hash_print", hash_print, METH_VARARGS, "hash print"},
        {nullptr,      nullptr, 0,               nullptr},
};

static struct PyModuleDef demoModule = {
        PyModuleDef_HEAD_INIT,
        "demo",   /* name of module */
        NULL, /* module documentation, may be NULL */
        -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
        MyDemoMethods
};

// 初始化模块
PyMODINIT_FUNC PyInit_demo(void) {
    return PyModule_Create(&demoModule);
}

编写测试程序测试

第一段函数调用,传入的参数为一个字典,遍历字典打印 keyvalue

第二段程序调用,传入的参数为一个 list,错误的数据类型,期待程序抛出 TypeError 的异常。

python
import demo

if __name__ == '__main__':
    demo.hash_print({'2': 333, 'd': 3333, 'x': 3})
    demo.hash_print([2, 3])

输出结果

从结果来看符合我们的预期,第一个 hash_print 遍历字典打印出我们需要数据,而且函数调用我们故意传入一个错误的数据类型,抛出错误。

('2', 333)
key: 2, value: 333
('d', 3333)
key: d, value: 3333
('x', 3)
key: x, value: 3
Traceback (most recent call last):
  File "run.py", line 5, in <module>
    demo.hash_print([2, 3])
TypeError: argument 1 must be dict, not list

在 C/C++ 中返回一个字典

调用 PyDict_New 构建一个字典,通过 PyDict_SetItemStringhash 中添加键值对,编译模块之后,导入模块测试 import demo,打印输出结果 print(demo.return_hash())

如下构造字典的方式主要在复杂场景下使用,如果我们知道返回的结果是什么的结构,可以通过这样的方式构建返回值 Py_BuildValue("{s:i,s:i}", "abc", 123, "def", 456),函数输出的结果为 {'abc': 123, 'def': 456}

c
static PyObject *return_hash(PyObject *self, PyObject *args) {
    // 创建一个字典对象
    PyObject *hash = PyDict_New();
    // 设置数值,这里可以增加 PyDict_SetItemString 的返回值判断,如果为 NULL 表示字典键值对添加失败
    PyDict_SetItemString(hash, "name", Py_BuildValue("s", "ok"));
    PyDict_SetItemString(hash, "gender", Py_BuildValue("s", "male"));
    PyDict_SetItemString(hash, "age", Py_BuildValue("i", 20));
    return hash;
}

总结

C/C++ 扩展 Python 模块,遍历字典其实就是参数的解析,然后获取字典的长度,然后遍历元祖数组,拿到键值对元祖,返回 hash 对象相对简单些,单纯的数据拼接。

涉及到函数就必须要了解函数的传参解析 (PyArg_ParseTuple),以及返回值的构造 (Py_BuildValue)