从同一个 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;
}

但是,我不确定是否应该将 selfNULL 或其他内容传递给该方法。

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_ 个调用。