从同一个 C 扩展模块调用不同方法的正确方法?
Proper way to call a different method from the same C-extension module?
我正在将纯 Python 模块转换为 C 扩展,以熟悉 C API。
Python实现如下:
_CRC_TABLE_ = [0] * 256
def initialize_crc_table():
if _CRC_TABLE_[1] != 0: # Safeguard against re-initialization
return
# snip
def calculate_crc(data: bytes, initial: int = 0) -> int:
if _CRC_TABLE_[1] == 0: # In case user forgets to initialize first
initialize_crc_table()
# snip
# additional non-CRC methods trimmed
到目前为止,我的 C 扩展有效:
#include <Python.h>
static Py_ssize_t CRC_TABLE_LEN = 256;
PyObject *_CRC_TABLE_;
static PyObject *method_initialize_crc_table(PyObject *self, PyObject *args) {
// snip
}
static PyMethodDef module_methods[] = {
{"initialize_crc_table", method_initialize_crc_table, METH_VARARGS, NULL},
{NULL, NULL, 0, NULL}
};
void _allocate_table_() {
_CRC_TABLE = PyList_New(CRC_TABLE_LEN);
PyObject *zero = Py_BuildValue("i", 0);
for (int i = 0; i < CRC_TABLE_LEN; i++) {
PyList_SetItem(_CRC_TABLE_, i, zero);
}
}
#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef module_utilities = {
PyModuleDef_HEAD_INIT,
"utilities",
NULL,
-1,
module_methods,
};
PyMODINIT_FUNC PyInit_utilities() {
PyObject *module = PyModule_Create(&module_utilities);
_allocate_table_();
PyModule_AddObject(module, "_CRC_TABLE", _CRC_TABLE_);
return module;
}
#else
PyMODINIT_FUNC initutilities() {
PyObject *module = Py_InitModule3("utilities", module_methods, NULL);
_allocate_table_();
PyModule_AddObject(module, "_CRC_TABLE", _CRC_TABLE_);
}
我能够从解释器中的 C 扩展访问 utilities._CRC_TABLE_
,并且在调用 utilities.intialize_crc_table
.
时值与 Python 等价物匹配
现在我尝试在 calculate_crc
开始时调用 initialize_crc_table
,执行与 Python 实施中使用的检查相同的检查。我暂时返回 None
:
static PyObject *method_calculate_crc(PyObject *self, PyObject *args) {
if (!(uint)PyLong_AsUnsignedLong(PyList_GetItem(_CRC_TABLE_, (Py_ssize_t) 1))) {
PyObject *call_initialize_crc_table = PyObject_GetAttrString(self, "initialize_crc_table");
PyObject_CallObject(call_initialize_crc_table, NULL);
Py_DECREF(call_initialize_crc_table);
}
Py_RETURN_NONE;
}
我已将它添加到 module_methods[]
并且它编译时没有警告或错误。当我在解释器中 运行 这个方法时,我得到了一个段错误。我认为这是因为 self
不是作为对象的模块。
我可以这样做作为替代方案,这似乎没有问题:
static PyObject *method_calculate_crc(PyObject *self, PyObject *args) {
if (!(uint)PyLong_AsUnsignedLong(PyList_GetItem(_CRC_TABLE_, (Py_ssize_t) 1))) {
method_initialize_crc_table(self, NULL);
}
Py_RETURN_NONE;
}
但是,我不确定是否应该将 self
、NULL
或其他内容传递给该方法。
从 method_calculate_crc
调用 method_initialize_crc_table
的正确方法是什么?
这里有一个 "gotcha" 我必须澄清一下。虽然代码是为 Python 3 而设计的,但开发最初是在 Python 2 中完成的,因为开发文件在我使用的机器上尚不可用。这揭示了每个版本处理事情的方式的一些差异。大卫的评论帮助导致了这一澄清。
如果方法被定义为 METH_VARARGS
但被定义为模块(相对于 class),Python 2 不会为 PyObject *self
参数传递任何内容.这个 是 在 documentation 中注明的,但如果您不小心,很容易忽略。 Python 3,但是,确实传递了一个指向模块的指针。正如 DavidW 所建议的,我实现了一个全局变量来保存对模块的引用。假设他关于 Python 在退出时处理取消引用的说法是正确的,我们可以安全地使用它来访问模块全局属性。
随着 PyObject *self
问题的解决,我们不再遇到段错误。然后我们可以解决以下问题:哪种方法(看起来更)适合在模块的本地范围内调用方法。我们这样做吗:
if (/* conditional */)
PyObject_CallMethod(module, "initialize_crc_table", NULL);
或者这个:
if (/* conditional */)
method_initialize_crc_table(self, args, kwargs);
基准似乎在这里提供了答案。使用 Python 内置的 timeit
模块,我们可以看到非常明显的性能差异。请注意,到目前为止,在我们的实现中,.calculate_crc
访问 ._CRC_TABLE_
并检查它是否已初始化,但没有进行任何处理。与 Python 2 和 3 相比的性能相同,因此被忽略。
命令如下:
python3 -m timeit "import utilities; utilities.calculate_crc(0)"
PyObject_CallMethod
:每个循环 874 纳秒
method_initialize_crc_table
:每个循环 44.3 微秒
据报道,使用 PyObject_
函数的速度提高了 50 倍,差异非常显着。单独的基准并不能促进什么是 "more correct",但如果没有明确的指导,它可能是我们使用的充分理由。因此,我将为该项目使用 PyObject_
个调用。
我正在将纯 Python 模块转换为 C 扩展,以熟悉 C API。
Python实现如下:
_CRC_TABLE_ = [0] * 256
def initialize_crc_table():
if _CRC_TABLE_[1] != 0: # Safeguard against re-initialization
return
# snip
def calculate_crc(data: bytes, initial: int = 0) -> int:
if _CRC_TABLE_[1] == 0: # In case user forgets to initialize first
initialize_crc_table()
# snip
# additional non-CRC methods trimmed
到目前为止,我的 C 扩展有效:
#include <Python.h>
static Py_ssize_t CRC_TABLE_LEN = 256;
PyObject *_CRC_TABLE_;
static PyObject *method_initialize_crc_table(PyObject *self, PyObject *args) {
// snip
}
static PyMethodDef module_methods[] = {
{"initialize_crc_table", method_initialize_crc_table, METH_VARARGS, NULL},
{NULL, NULL, 0, NULL}
};
void _allocate_table_() {
_CRC_TABLE = PyList_New(CRC_TABLE_LEN);
PyObject *zero = Py_BuildValue("i", 0);
for (int i = 0; i < CRC_TABLE_LEN; i++) {
PyList_SetItem(_CRC_TABLE_, i, zero);
}
}
#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef module_utilities = {
PyModuleDef_HEAD_INIT,
"utilities",
NULL,
-1,
module_methods,
};
PyMODINIT_FUNC PyInit_utilities() {
PyObject *module = PyModule_Create(&module_utilities);
_allocate_table_();
PyModule_AddObject(module, "_CRC_TABLE", _CRC_TABLE_);
return module;
}
#else
PyMODINIT_FUNC initutilities() {
PyObject *module = Py_InitModule3("utilities", module_methods, NULL);
_allocate_table_();
PyModule_AddObject(module, "_CRC_TABLE", _CRC_TABLE_);
}
我能够从解释器中的 C 扩展访问 utilities._CRC_TABLE_
,并且在调用 utilities.intialize_crc_table
.
现在我尝试在 calculate_crc
开始时调用 initialize_crc_table
,执行与 Python 实施中使用的检查相同的检查。我暂时返回 None
:
static PyObject *method_calculate_crc(PyObject *self, PyObject *args) {
if (!(uint)PyLong_AsUnsignedLong(PyList_GetItem(_CRC_TABLE_, (Py_ssize_t) 1))) {
PyObject *call_initialize_crc_table = PyObject_GetAttrString(self, "initialize_crc_table");
PyObject_CallObject(call_initialize_crc_table, NULL);
Py_DECREF(call_initialize_crc_table);
}
Py_RETURN_NONE;
}
我已将它添加到 module_methods[]
并且它编译时没有警告或错误。当我在解释器中 运行 这个方法时,我得到了一个段错误。我认为这是因为 self
不是作为对象的模块。
我可以这样做作为替代方案,这似乎没有问题:
static PyObject *method_calculate_crc(PyObject *self, PyObject *args) {
if (!(uint)PyLong_AsUnsignedLong(PyList_GetItem(_CRC_TABLE_, (Py_ssize_t) 1))) {
method_initialize_crc_table(self, NULL);
}
Py_RETURN_NONE;
}
但是,我不确定是否应该将 self
、NULL
或其他内容传递给该方法。
从 method_calculate_crc
调用 method_initialize_crc_table
的正确方法是什么?
这里有一个 "gotcha" 我必须澄清一下。虽然代码是为 Python 3 而设计的,但开发最初是在 Python 2 中完成的,因为开发文件在我使用的机器上尚不可用。这揭示了每个版本处理事情的方式的一些差异。大卫的评论帮助导致了这一澄清。
如果方法被定义为 METH_VARARGS
但被定义为模块(相对于 class),Python 2 不会为 PyObject *self
参数传递任何内容.这个 是 在 documentation 中注明的,但如果您不小心,很容易忽略。 Python 3,但是,确实传递了一个指向模块的指针。正如 DavidW 所建议的,我实现了一个全局变量来保存对模块的引用。假设他关于 Python 在退出时处理取消引用的说法是正确的,我们可以安全地使用它来访问模块全局属性。
随着 PyObject *self
问题的解决,我们不再遇到段错误。然后我们可以解决以下问题:哪种方法(看起来更)适合在模块的本地范围内调用方法。我们这样做吗:
if (/* conditional */)
PyObject_CallMethod(module, "initialize_crc_table", NULL);
或者这个:
if (/* conditional */)
method_initialize_crc_table(self, args, kwargs);
基准似乎在这里提供了答案。使用 Python 内置的 timeit
模块,我们可以看到非常明显的性能差异。请注意,到目前为止,在我们的实现中,.calculate_crc
访问 ._CRC_TABLE_
并检查它是否已初始化,但没有进行任何处理。与 Python 2 和 3 相比的性能相同,因此被忽略。
命令如下:
python3 -m timeit "import utilities; utilities.calculate_crc(0)"
PyObject_CallMethod
:每个循环 874 纳秒
method_initialize_crc_table
:每个循环 44.3 微秒
据报道,使用 PyObject_
函数的速度提高了 50 倍,差异非常显着。单独的基准并不能促进什么是 "more correct",但如果没有明确的指导,它可能是我们使用的充分理由。因此,我将为该项目使用 PyObject_
个调用。