如何支持 Pybind11 创建的 Python 枚举的 len() 方法

Gup*_*pta 5 c++ enums pybind11

假设我有一个像这样的 C++ 枚举:

enum class Kind {Kind1 = 1, Kind2, Kind3};
Run Code Online (Sandbox Code Playgroud)

要使用 Pybind11 将此枚举绑定到 Python 枚举中,我正在执行以下操作:

py::enum_<Kind>(py_module, "Kind")
   .value("Kind1", Kind::Kind1)
   .value("Kind2", Kind::Kind2)
   .value("Kind3", Kind::Kind3)
   .def("__len__", 
      [](Kind p) {
         return 3;
      });
Run Code Online (Sandbox Code Playgroud)

编译代码后,如果我询问枚举的长度,我将收到此错误:

>>> len(Kind)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'pybind11_type' has no len()
Run Code Online (Sandbox Code Playgroud)

我有什么想法可以解决它吗?

编辑1:我在 Visual Studio 2019 (C++17) 上使用 Pybind11 版本 2.10.1。

编辑 2:我希望具有与 Python 枚举中相同的行为:

>>> from enum import Enum
>>> class Kind(Enum):
...    kind1 = 1
...    kind2 = 2
...    kind3 = 3
...
>>> len(Kind)
3
Run Code Online (Sandbox Code Playgroud)

Dan*_*šek 2

在开始之前,让我们解决一个相关问题——如何避免必须显式指定每个枚举返回的长度?如果有什么东西可以为我们做到这一点,或者如果我们可以在运行时动态计算它,那就太好了。

原来有办法。在阅读代码时,我发现了一个有趣的实现细节。pybind11 创建的枚举包装类有一个名为 的属性__entries。它是一本字典,每个枚举值保存一个条目,主要用于生成文档、获取值的文本表示形式以及将值导出到父范围。

以下是示例枚举的样子:

>>> print(Kind.__entries)
{'Kind1': (Kind.Kind1, None), 'Kind2': (Kind.Kind2, None), 'Kind3': (Kind.Kind3, None)}
Run Code Online (Sandbox Code Playgroud)

因此,我们可以len(Kind.__entries)在运行时获取正确的长度(枚举值的数量)。在 C++ 中,这就是类对象py::len(cls.attr("__entries"))所在的位置。clsKind


现在我们可以找到问题的根源——如何len在类对象而不是类实例上进行工作。根据这个 SO 答案,实现这一点的一种方法是使用元类。具体来说,我们需要枚举包装类使用具有__len__成员函数的元类,该元类将计算并返回包装类保存的值的数量。

事实证明,pybind 生成的包装类已经使用了名为的自定义元类pybind11_type

>>> type(Kind)
<class 'pybind11_builtins.pybind11_type'>
Run Code Online (Sandbox Code Playgroud)

因此,方法是创建一个新的元类,例如pybind11_ext_enum,它派生自pybind11_type,并提供缺失的__len__


下一个问题是,我们如何从 C++ 创建这样的元类。Pybind11 没有提供任何方便的功能来执行此操作,因此我们必须自己完成。为此,我们需要:

  1. 代表原始 pybind11 元类的对象pybind11_type。我发现它藏在里面internals,所以我从那里抓住了它。

    py::object get_pybind11_metaclass()
    {
        auto &internals = py::detail::get_internals();
        return py::reinterpret_borrow<py::object>((PyObject*)internals.default_metaclass);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. type代表标准 Python 元类(即在 CPython API 中)的对象PyType_Type

    py::object get_standard_metaclass()
    {
        auto &internals = py::detail::get_internals();
        return py::reinterpret_borrow<py::object>((PyObject *)&PyType_Type);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  3. 我们希望这个新类具有的属性字典。这只需要一个条目来定义我们的__len__方法。

    py::dict attributes;
    attributes["__len__"] = py::cpp_function(
        [](py::object cls) {
            return py::len(cls.attr("__entries"));
        }
        , py::is_method(py::none())
        );
    
    Run Code Online (Sandbox Code Playgroud)
  4. 用于type创建我们的新类对象。

    auto pybind11_metaclass = get_pybind11_metaclass();
    auto standard_metaclass = get_standard_metaclass();
    return standard_metaclass(std::string("pybind11_ext_enum")
        , py::make_tuple(pybind11_metaclass)
        , attributes);
    
    Run Code Online (Sandbox Code Playgroud)

我们可以将第 3 部分和第 4 部分放入一个函数中:py::object create_enum_metaclass() { ... }


最后,我们在创建枚举包装器时必须使用新的元类。

>>> print(Kind.__entries)
{'Kind1': (Kind.Kind1, None), 'Kind2': (Kind.Kind2, None), 'Kind3': (Kind.Kind3, None)}
Run Code Online (Sandbox Code Playgroud)

现在我们可以在 Python 中使用它:

>>> from so07 import Kind
>>> type(Kind)
<class 'importlib._bootstrap.pybind11_ext_enum'>
>>> len(Kind)
3
Run Code Online (Sandbox Code Playgroud)