【问题标题】:How to create an Enum object in Python C API?如何在 Python C API 中创建枚举对象?
【发布时间】:2021-09-08 21:28:36
【问题描述】:

我正在努力如何在 Python C API 中创建一个 Python Enum 对象。枚举类已将tp_base 分配给PyEnum_Type,因此它继承了Enum。但是,我想不出一种方法来告诉 Enum 基类枚举中有哪些项目。我想允许使用每个 Python Enum 提供的 __members__ 属性从 Python 进行迭代和查找。

谢谢,

杰勒

【问题讨论】:

  • 您是要创建Enum instance 还是新的Enum type
  • 我正在尝试声明一个枚举类型,例如“Season”或“Month”。如果我理解正确的话,Enum 中的每个项目都代表该 Enum 的一个静态实例,对吧?
  • 枚举不是用C创建的,它是用Python创建的,这是一个重要的区别。
  • this question 有帮助吗?
  • 就我个人而言,我可能会使用PyRun_SimpleString 或类似的方法——我们确实可以从 C API 访问 Python 解释器,所以为什么不将它用于这样的事情。不过,我不愿意将其作为答案发布(因为它显然不是“问题的精神”)

标签: python c python-c-api


【解决方案1】:

编辑说明:An answer on a very similar question 详细说明了enum.Enum 如何具有可替代使用的功能接口。这几乎可以肯定是正确的方法。我认为我在这里的回答是一种有用的替代方法,尽管它可能不是解决这个问题的最佳方法。


我知道这个答案有点作弊,但这正是用 Python 编写的更好的代码,在 C API 中,我们仍然可以访问完整的 Python 解释器。我对此的理由是,将内容完全保留在 C 中的主要原因是性能,而且创建枚举对象似乎不太可能对性能至关重要。

我将给出三个版本,基本上取决于复杂程度。


首先,最简单的情况:枚举是完全已知的并在编译时定义。这里我们简单设置一个空的全局dict,运行Python代码,然后从全局dict中提取枚举:

PyObject* get_enum(void) {
    const char str[] = "from enum import Enum\n"
                       "class Colour(Enum):\n"
                       "    RED = 1\n"
                       "    GREEN = 2\n"
                       "    BLUE = 3\n"
                       "";
    PyObject *global_dict=NULL, *should_be_none=NULL, *output=NULL;
    global_dict = PyDict_New();
    if (!global_dict) goto cleanup;
    should_be_none = PyRun_String(str, Py_file_input, global_dict, global_dict);
    if (!should_be_none) goto cleanup;
    // extract Color from global_dict
    output = PyDict_GetItemString(global_dict, "Colour");
    if (!output) {
        // PyDict_GetItemString does not set exceptions
        PyErr_SetString(PyExc_KeyError, "could not get 'Colour'");
    } else {
        Py_INCREF(output); // PyDict_GetItemString returns a borrow reference
    }
    cleanup:
    Py_XDECREF(global_dict);
    Py_XDECREF(should_be_none);
    return output;
}

其次,我们可能想在运行时更改我们在 C 中定义的内容。例如,输入参数可能会选择枚举值。在这里,我将使用字符串格式化将适当的值插入到我们的字符串中。这里有许多选项:sprintfPyBytes_Format,C++ 标准库,使用 Python 字符串(可能还调用 Python 代码?)。选择您最喜欢的。

PyObject* get_enum_fmt(int red, int green, int blue) {
    const char str[] = "from enum import Enum\n"
                       "class Colour(Enum):\n"
                       "    RED = %d\n"
                       "    GREEN = %d\n"
                       "    BLUE = %d\n"
                       "";
    PyObject *formatted_str=NULL, *global_dict=NULL, *should_be_none=NULL, *output=NULL;

    formatted_str = PyBytes_FromFormat(str, red, green, blue);
    if (!formatted_str) goto cleanup;
    global_dict = PyDict_New();
    if (!global_dict) goto cleanup;
    should_be_none = PyRun_String(PyBytes_AsString(formatted_str), Py_file_input, global_dict, global_dict);
    if (!should_be_none) goto cleanup;
    // extract Color from global_dict
    output = PyDict_GetItemString(global_dict, "Colour");
    if (!output) {
        // PyDict_GetItemString does not set exceptions
        PyErr_SetString(PyExc_KeyError, "could not get 'Colour'");
    } else {
        Py_INCREF(output); // PyDict_GetItemString returns a borrow reference
    }
    cleanup:
    Py_XDECREF(formatted_str);
    Py_XDECREF(global_dict);
    Py_XDECREF(should_be_none);
    return output;
}

显然,您可以随心所欲地使用字符串格式——我刚刚选择了一个简单的例子来说明这一点。与之前版本的主要区别在于调用PyBytes_FromFormat 来设置字符串,以及调用PyBytes_AsString 从准备好的bytes 对象中获取底层char*


最后,我们可以在 C Python dict 中准备枚举属性并将其传入。这需要进行一些更改。本质上我使用@AnttiHaapala 的低级Python 代码,但在调用__prepare__ 之后插入namespace.update(contents)


PyObject* get_enum_dict(const char* key1, int value1, const char* key2, int value2) {
    const char str[] = "from enum import Enum\n"
                       "name = 'Colour'\n"
                       "bases = (Enum,)\n"
                       "enum_meta = type(Enum)\n"
                       "namespace = enum_meta.__prepare__(name, bases)\n"
                       "namespace.update(contents)\n"
                       "Colour = enum_meta(name, bases, namespace)\n";

    PyObject *global_dict=NULL, *contents_dict=NULL, *value_as_object=NULL, *should_be_none=NULL, *output=NULL;
    global_dict = PyDict_New();
    if (!global_dict) goto cleanup;

    // create and fill the contents dictionary
    contents_dict = PyDict_New();
    if (!contents_dict) goto cleanup;
    value_as_object = PyLong_FromLong(value1);
    if (!value_as_object) goto cleanup;
    int set_item_result = PyDict_SetItemString(contents_dict, key1, value_as_object);
    Py_CLEAR(value_as_object);
    if (set_item_result!=0) goto cleanup;
    value_as_object = PyLong_FromLong(value2);
    if (!value_as_object) goto cleanup;
    set_item_result = PyDict_SetItemString(contents_dict, key2, value_as_object);
    Py_CLEAR(value_as_object);
    if (set_item_result!=0) goto cleanup;

    set_item_result = PyDict_SetItemString(global_dict, "contents", contents_dict);
    if (set_item_result!=0) goto cleanup;

    should_be_none = PyRun_String(str, Py_file_input, global_dict, global_dict);
    if (!should_be_none) goto cleanup;
    // extract Color from global_dict
    output = PyDict_GetItemString(global_dict, "Colour");
    if (!output) {
        // PyDict_GetItemString does not set exceptions
        PyErr_SetString(PyExc_KeyError, "could not get 'Colour'");
    } else {
        Py_INCREF(output); // PyDict_GetItemString returns a borrow reference
    }
    cleanup:
    Py_XDECREF(contents_dict);
    Py_XDECREF(global_dict);
    Py_XDECREF(should_be_none);
    return output;
}

同样,这提供了一种相当灵活的方法来将 C 中的值获取到生成的枚举中。


为了测试,我使用了以下简单的 Cython 包装器 - 这只是为了帮助人们尝试这些功能的完整性。

cdef extern from "cenum.c":
    object get_enum()
    object get_enum_fmt(int, int, int)
    object get_enum_dict(char*, int, char*, int)


def py_get_enum():
    return get_enum()

def py_get_enum_fmt(red, green, blue):
    return get_enum_fmt(red, green, blue)

def py_get_enum_dict(key1, value1, key2, value2):
    return get_enum_dict(key1, value1, key2, value2)

重申一下:这个答案只是部分在 C API 中,但是从 C 调用 Python 的方法是我发现有时对于完全用 C 编写的“运行一次”代码很有成效的方法.

【讨论】:

  • 谢谢 ;D 我正在考虑是否应该尝试自己在 C 中编写代码,但幸运的是我不需要再考虑了
【解决方案2】:

这并不简单。 Enum 是一个使用Python 元类Python 类。可以在 C 中创建它,但它只是模拟在 C 中构造 Python 代码 - 最终结果是相同的,虽然它稍微加快了速度,但您很可能会运行代码每个程序只运行一次。

无论如何可能的,但这一点都不容易。我将展示如何在 Python 中做到这一点:

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

print(Color)
print(Color.RED)

等同于:

from enum import Enum

name = 'Color'
bases = (Enum,)
enum_meta = type(Enum)

namespace = enum_meta.__prepare__(name, bases)
namespace['RED'] = 1
namespace['GREEN'] = 2
namespace['BLUE'] = 3

Color = enum_meta(name, bases, namespace)

print(Color)
print(Color.RED)

后者是你需要翻译成C的代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多