如何编写一个`Python C扩展`:从`C语言`层面操作`Python`对象、处理引用计数。

Python C 扩展开发:Python 对象操作与引用计数

各位同学,大家好!今天我们来深入探讨 Python C 扩展的开发,重点关注如何在 C 语言层面操作 Python 对象以及正确处理引用计数。这部分内容是编写高效且稳定的 Python C 扩展的关键。

1. Python C 扩展基础回顾

首先,我们简单回顾一下 Python C 扩展的基本概念。Python 解释器是用 C 语言编写的,因此我们可以使用 C 语言编写扩展模块,从而利用 C 语言的性能优势,或者调用已有的 C/C++ 库。

Python 提供了一套 C API,允许我们创建、访问和操作 Python 对象。一个典型的 Python C 扩展模块包含以下几个部分:

  • 头文件: 必须包含 Python.h,它定义了所有必要的类型、函数和宏。
  • 模块初始化函数: 这是一个特殊的函数,当 Python 导入模块时会被调用。它负责注册模块中的函数和类。通常命名为 PyInit_<module_name>,例如 PyInit_my_module
  • 模块函数: 这些是 C 函数,会被 Python 代码调用。它们接收 Python 对象作为参数,并返回 Python 对象作为结果。
  • 错误处理: C 扩展需要处理可能发生的错误,并向 Python 解释器报告。
  • 引用计数管理: 这是最关键的部分。Python 使用引用计数来管理内存。C 扩展必须正确地增加和减少对象的引用计数,以避免内存泄漏或程序崩溃。

2. Python 对象在 C 语言中的表示

在 C 语言中,Python 对象由 PyObject 结构体表示。PyObject 是一个通用对象,所有 Python 对象(例如整数、字符串、列表、字典等)都继承自它。PyObject 的定义如下(简化版本):

typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

其中:

  • _PyObject_HEAD_EXTRA:这是一个宏,用于调试构建,通常为空。
  • ob_refcnt:这是一个整数,表示对象的引用计数。
  • ob_type:这是一个指向 PyTypeObject 结构体的指针,表示对象的类型。

PyTypeObject 结构体描述了对象的类型,包括类型名称、大小、方法等。

由于 PyObject 是所有 Python 对象的基类,因此我们可以将任何 Python 对象强制转换为 PyObject*。但是,要访问对象的特定属性或方法,我们需要将其转换为更具体的类型。

3. 常用的 Python 对象类型

Python 提供了一系列 C API,用于创建和操作不同类型的 Python 对象。下面是一些常用的对象类型及其对应的 C API:

Python 类型 C 类型 创建函数 访问函数
整数 PyLongObject PyLong_FromLong, PyLong_FromUnsignedLong PyLong_AsLong, PyLong_AsUnsignedLong, PyLong_AsLongLong, PyLong_AsUnsignedLongLong
浮点数 PyFloatObject PyFloat_FromDouble PyFloat_AsDouble
字符串 PyUnicodeObject PyUnicode_FromString, PyUnicode_FromStringAndSize PyUnicode_AsUTF8, PyUnicode_GetLength
列表 PyListObject PyList_New PyList_GetItem, PyList_SetItem, PyList_Append, PyList_Insert, PyList_Size
元组 PyTupleObject PyTuple_New PyTuple_GetItem, PyTuple_SetItem, PyTuple_Size
字典 PyDictObject PyDict_New PyDict_GetItem, PyDict_SetItem, PyDict_DelItem, PyDict_Contains, PyDict_Keys, PyDict_Values, PyDict_Items, PyDict_Size
None Py_None

示例:创建和操作整数对象

#include <Python.h>

PyObject* create_int(long value) {
    PyObject* obj = PyLong_FromLong(value);
    return obj;
}

long get_int_value(PyObject* obj) {
    if (!PyLong_Check(obj)) {
        PyErr_SetString(PyExc_TypeError, "Expected an integer");
        return -1; // Or any other error indicator
    }
    return PyLong_AsLong(obj);
}

// 模块函数示例
static PyObject* my_module_add(PyObject *self, PyObject *args) {
    long a, b, result;
    PyObject *result_obj;

    // 解析参数
    if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
        return NULL; // 参数解析失败
    }

    result = a + b;
    result_obj = PyLong_FromLong(result);
    return result_obj;
}

// 模块初始化函数
static PyMethodDef MyModuleMethods[] = {
    {"add",  my_module_add, METH_VARARGS, "Add two integers."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef mymodule = {
    PyModuleDef_HEAD_INIT,
    "my_module",   /* 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. */
    MyModuleMethods
};

PyMODINIT_FUNC
PyInit_my_module(void)
{
    return PyModule_Create(&mymodule);
}

在这个例子中,create_int 函数使用 PyLong_FromLong 创建一个整数对象。get_int_value 函数使用 PyLong_AsLong 获取整数对象的值。注意,在使用 PyLong_AsLong 之前,需要使用 PyLong_Check 检查对象是否为整数类型。如果类型不匹配,应该设置错误并返回一个错误指示。

示例:创建和操作字符串对象

#include <Python.h>

PyObject* create_string(const char* str) {
    PyObject* obj = PyUnicode_FromString(str);
    return obj;
}

const char* get_string_value(PyObject* obj) {
    if (!PyUnicode_Check(obj)) {
        PyErr_SetString(PyExc_TypeError, "Expected a string");
        return NULL;
    }
    return PyUnicode_AsUTF8(obj);
}

在这个例子中,create_string 函数使用 PyUnicode_FromString 创建一个字符串对象。get_string_value 函数使用 PyUnicode_AsUTF8 获取字符串对象的值。同样,在使用 PyUnicode_AsUTF8 之前,需要使用 PyUnicode_Check 检查对象是否为字符串类型。

4. Python 引用计数

Python 使用引用计数来管理内存。每个对象都有一个引用计数,表示有多少个变量或对象引用了它。当引用计数变为 0 时,Python 解释器会自动释放对象的内存。

C 扩展必须正确地管理 Python 对象的引用计数,以避免内存泄漏或程序崩溃。以下是一些需要注意的事项:

  • 创建新对象: 当你创建一个新的 Python 对象时,其引用计数被设置为 1。你需要负责在不再需要该对象时减少其引用计数。
  • 增加引用计数: 当你增加一个对象的引用时,你需要调用 Py_INCREF(obj) 来增加其引用计数。
  • 减少引用计数: 当你减少一个对象的引用时,你需要调用 Py_DECREF(obj) 来减少其引用计数。
  • 返回值: 当你的 C 函数返回一个 Python 对象时,你需要决定是否转移该对象的所有权。如果转移所有权,则不需要减少其引用计数。如果不转移所有权,则需要增加其引用计数。
  • 窃取引用: 有些函数会“窃取”对传递给它们的对象的引用。这意味着调用者不再拥有该对象的引用,也不需要减少其引用计数。文档会明确指出函数是否窃取引用。

引用计数规则总结

操作 引用计数变化 责任
创建新对象 (e.g., PyLong_FromLong) +1 调用者负责在不再需要时 Py_DECREF
函数返回新对象 +1 调用者负责在不再需要时 Py_DECREF (如果未转移所有权)
增加引用 (e.g., Py_INCREF) +1 调用者负责在之后 Py_DECREF
减少引用 (e.g., Py_DECREF) -1 当不再需要对象时调用
函数“窃取”引用 -1 调用者不需要 Py_DECREF

示例:引用计数管理

#include <Python.h>

PyObject* create_list_with_int(long value) {
    PyObject* list = PyList_New(1);
    if (list == NULL) {
        return NULL; // Failed to create list
    }

    PyObject* int_obj = PyLong_FromLong(value);
    if (int_obj == NULL) {
        Py_DECREF(list); // Clean up list if creating int fails
        return NULL; // Failed to create int
    }

    // PyList_SetItem steals a reference to int_obj
    PyList_SetItem(list, 0, int_obj);

    return list;
}

PyObject* get_list_item_and_increment(PyObject* list, int index) {
    PyObject* item = PyList_GetItem(list, index);
    if (item == NULL) {
        return NULL;
    }

    Py_INCREF(item); // Increment the reference count before returning

    return item;
}

static PyObject* my_module_return_new_list(PyObject *self, PyObject *args) {
    PyObject *new_list = PyList_New(0); // 创建一个新列表

    if (new_list == NULL) {
        return NULL; // 内存分配失败
    }

    // 在这里对列表进行一些操作,例如添加元素

    return new_list; // 返回新列表,引用计数为 1,需要调用者负责 `Py_DECREF`
}

static PyObject* my_module_return_existing_list(PyObject *self, PyObject *args) {
    static PyObject *existing_list = NULL; // 静态列表

    if (existing_list == NULL) {
        existing_list = PyList_New(0); // 创建一个新列表(仅在第一次调用时)
        if (existing_list == NULL) {
            return NULL; // 内存分配失败
        }
        // 在这里对列表进行一些操作,例如添加元素
    }

    Py_INCREF(existing_list); // 增加引用计数,因为要返回它
    return existing_list; // 返回静态列表,引用计数增加,需要调用者负责 `Py_DECREF`
}

//模块函数示范 释放资源
static PyObject* my_module_example_function(PyObject *self, PyObject *args) {
    PyObject *obj = NULL;

    // 创建一个 Python 对象
    obj = PyLong_FromLong(42);
    if (obj == NULL) {
        return NULL; // 创建失败
    }

    // 使用该对象...

    // 释放对象
    Py_DECREF(obj); // 减少引用计数

    Py_RETURN_NONE; // 返回 None
}

在这个例子中,create_list_with_int 函数创建一个列表和一个整数对象。PyList_SetItem 函数“窃取”对整数对象的引用,因此我们不需要减少其引用计数。然而,如果 PyList_SetItem 失败,我们需要手动减少列表和整数对象的引用计数。

get_list_item_and_increment 函数返回列表中的一个元素。在返回之前,我们需要增加该元素的引用计数,因为调用者将获得该元素的引用。

my_module_return_new_list 函数返回新创建的列表,需要调用者负责 Py_DECREF

my_module_return_existing_list 函数返回静态列表,调用前增加了引用计数,需要调用者负责 Py_DECREF

my_module_example_function 函数创建了一个对象,用完后立即释放。

5. 错误处理

C 扩展需要处理可能发生的错误,并向 Python 解释器报告。Python 提供了一系列 C API,用于设置和清除错误。

常用的错误处理函数包括:

  • PyErr_SetString(PyObject* type, const char* message):设置一个错误。type 是一个异常类型(例如 PyExc_TypeErrorPyExc_ValueError),message 是错误消息。
  • PyErr_SetObject(PyObject* type, PyObject* value):设置一个错误。type 是一个异常类型,value 是异常对象。
  • PyErr_Clear():清除当前线程的错误指示器。
  • PyErr_Occurred():检查是否发生了错误。如果发生了错误,则返回一个异常对象;否则返回 NULL
  • PyErr_ExceptionMatches(PyObject* exc, PyObject* type): 检查发生的异常是否匹配给定的类型。

当你的 C 函数遇到错误时,应该设置一个错误并返回 NULL(如果函数返回一个 Python 对象)或一个错误指示(例如 -1)。

示例:错误处理

#include <Python.h>

PyObject* divide(PyObject* self, PyObject* args) {
    double a, b, result;

    if (!PyArg_ParseTuple(args, "dd", &a, &b)) {
        return NULL; // 参数解析失败
    }

    if (b == 0) {
        PyErr_SetString(PyExc_ZeroDivisionError, "Cannot divide by zero");
        return NULL; // 除数为零
    }

    result = a / b;
    return PyFloat_FromDouble(result);
}

//模块初始化函数的修改

static PyMethodDef MyModuleMethods[] = {
    {"divide",  divide, METH_VARARGS, "Divide two numbers."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef mymodule = {
    PyModuleDef_HEAD_INIT,
    "my_module",   /* 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. */
    MyModuleMethods
};

PyMODINIT_FUNC
PyInit_my_module(void)
{
    return PyModule_Create(&mymodule);
}

在这个例子中,divide 函数检查除数是否为零。如果是,则设置一个 PyExc_ZeroDivisionError 异常并返回 NULL

6. 总结

今天我们学习了 Python C 扩展开发中操作 Python 对象和处理引用计数的关键概念。理解 PyObject 结构体,掌握常用的 Python 对象类型及其 C API,并严格遵守引用计数规则,是编写健壮 C 扩展的基础。 另外,我们还学习了如何正确处理 C 扩展中的错误,确保扩展在出现问题时能够向 Python 解释器报告,避免程序崩溃。只有掌握这些核心概念,才能编写出高效、稳定的 Python C 扩展。

进一步学习

希望今天的讲座对大家有所帮助!

引用计数和异常处理:保证扩展的稳定

合理使用 Py_INCREFPy_DECREF 管理内存,避免泄漏,同时使用 PyErr_SetString 等函数报告错误,保证 Python 扩展的稳定性和可靠性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注