英文:
How to support len() method for Python enums created by Pybind11
问题
假设我有一个C++枚举,如下所示:
enum class Kind {Kind1 = 1, Kind2, Kind3};
要使用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;
});
编译完代码后,如果我请求枚举的长度,我将得到这个错误:
>>> len(Kind)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'pybind11_type' has no len()
有任何解决方法吗?
编辑1: 我正在使用Pybind11版本2.10.1,Visual Studio 2019(C++17)。
编辑2: 我希望它的行为与Python枚举相同:
>>> from enum import Enum
>>> class Kind(Enum):
... kind1 = 1
... kind2 = 2
... kind3 = 3
...
>>> len(Kind)
3
英文:
Suppose that I have a C++ enumeration like this:
enum class Kind {Kind1 = 1, Kind2, Kind3};
to bind this enumeration into a Python enumeration using Pybind11, I am doing something like this:
py::enum_<Kind>(py_module, "Kind")
.value("Kind1", Kind::Kind1)
.value("Kind2", Kind::Kind2)
.value("Kind3", Kind::Kind3)
.def("__len__",
[](Kind p) {
return 3;
});
After compiling the code, if I ask for the length of the enumration, I will get this error:
>>> len(Kind)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'pybind11_type' has no len()
Any ideas how I can fix it?
Edit 1: I am using Pybind11 version 2.10.1 on Visual Studio 2019 (C++17).
Edit 2: I want to have the same behavior as it is in Python Enums:
>>> from enum import Enum
>>> class Kind(Enum):
... kind1 = 1
... kind2 = 2
... kind3 = 3
...
>>> len(Kind)
3
答案1
得分: 2
在我们开始之前,让我们解决一个相关的问题——如何避免在每个枚举中明确指定要返回的长度?如果有什么东西可以为我们做这个工作,或者在运行时动态计算它会很好。
原来有一种方法。在阅读代码时,我发现了一个有趣的实现细节。pybind11创建的枚举包装类具有名为__entries
的属性。它是一个字典,每个枚举值都有一个条目,主要用于生成文档、获取值的文本表示以及将值导出到父作用域。
以下是你的示例枚举的外观:
>>> print(Kind.__entries)
{'Kind1': (Kind.Kind1, None), 'Kind2': (Kind.Kind2, None), 'Kind3': (Kind.Kind3, None)}
因此,我们可以使用len(Kind.__entries)
在运行时获取正确的长度(枚举值的数量)。在C++中,这将是py::len(cls.attr("__entries"))
,其中cls
是Kind
类对象。
现在我们可以解决问题的根本问题了——如何使len
适用于类对象,而不是类实例。根据这个Stack Overflow回答中的说明,实现这一点的一种方法是使用元类。具体来说,我们需要使枚举包装类使用一个具有__len__
成员函数的元类,该函数将计算并返回包装类包含的值的数量。
事实证明,pybind生成的包装类已经使用了一个名为pybind11_type
的自定义元类:
>>> type(Kind)
<class 'pybind11_builtins.pybind11_type'>
因此,方法是创建一个新的元类,例如pybind11_ext_enum
,它继承自pybind11_type
,并提供缺失的__len__
。
接下来的问题是,我们如何从C++中创建这样的元类。Pybind11没有提供任何方便的功能来做到这一点,所以我们必须自己来做。为此,我们需要:
- 一个表示原始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);
}
- 一个表示标准Python元类
type
(在CPython API中即PyType_Type
)的对象。
py::object get_standard_metaclass()
{
auto &internals = py::detail::get_internals();
return py::reinterpret_borrow<py::object>((PyObject *)&PyType_Type);
}
- 一个包含我们希望这个新类具有的属性的字典。这只需要一个条目,定义我们的
__len__
方法。
py::dict attributes;
attributes["__len__"] = py::cpp_function(
[](py::object cls) {
return py::len(cls.attr("__entries"));
},
py::is_method(py::none())
);
- 使用
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);
我们可以将第3和第4部分放入一个函数中:py::object create_enum_metaclass() { ... }
。
最后,我们必须在创建枚举包装类时使用我们的新元类。
PYBIND11_MODULE(so07, m)
{
auto enum_metaclass = create_enum_metaclass();
py::enum_<Kind>(m, "Kind", py::metaclass(enum_metaclass))
.value("Kind1", Kind::Kind1)
.value("Kind2", Kind::Kind2)
.value("Kind3", Kind::Kind3);
}
现在我们可以在Python中使用它:
>>> from so07 import Kind
>>> type(Kind)
<class 'importlib._bootstrap.pybind11_ext_enum'>
>>> len(Kind)
3
英文:
Before we begin, let's solve one related problem -- how can we avoid having to explicitly specify the length to return for each enumeration? It would be nice if something did it for us, or perhaps if we could calculate it dynamically during runtime.
Turns out there is a way. While reading through the code, I came across an interesting implementation detail. The enumeration wrapper class that pybind11 creates has an attribute named __entries
. It is a dictionary, which holds one entry per enumeration value, mostly used to generate documentation, get text representation of the value, and export the values to parent scope.
Here is what it looks like for your example enum:
>>> print(Kind.__entries)
{'Kind1': (Kind.Kind1, None), 'Kind2': (Kind.Kind2, None), 'Kind3': (Kind.Kind3, None)}
Hence, we can use len(Kind.__entries)
to get the correct length (number of enumeration values) at runtime. In C++ this would be py::len(cls.attr("__entries"))
where cls
is the Kind
class object.
Now we can get to the root of the issue -- how can we make len
work on a class object, rather than a class instance. According to this SO answer, one way to accomplish that is using a metaclass. Specifically, we need the enum wrapper class to use a metaclass that has a __len__
member function, which will calculate and return the number of values the wrapper class holds.
It turns out that the wrapper classes generated by pybind already use a custom metaclass named pybind11_type
:
>>> type(Kind)
<class 'pybind11_builtins.pybind11_type'>
Hence, the approach would be to create a new metaclass, say pybind11_ext_enum
, which would be derived from pybind11_type
, and provide the missing __len__
.
The next question is, how can we create such metaclass from c++. Pybind11 doesn't provide any convenience functionality to do this, so we'll have to do it ourselves. To do so, we need:
-
An object representing the original pybind11 metaclass
pybind11_type
. I found it stashed ininternals
, so I grab it from there.py::object get_pybind11_metaclass() { auto &internals = py::detail::get_internals(); return py::reinterpret_borrow<py::object>((PyObject*)internals.default_metaclass); }
-
An object representing the standard Python metaclass
type
(i.e.PyType_Type
in CPython API).py::object get_standard_metaclass() { auto &internals = py::detail::get_internals(); return py::reinterpret_borrow<py::object>((PyObject *)&PyType_Type); }
-
A dictionary of attributes we want this new class to have. This needs only a single entry, defining our
__len__
method.py::dict attributes; attributes["__len__"] = py::cpp_function( [](py::object cls) { return py::len(cls.attr("__entries")); } , py::is_method(py::none()) );
-
Use
type
to create our new class object.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);
We can put parts 3 and 4 into a function: py::object create_enum_metaclass() { ... }
.
Finally, we have to use our new metaclass when creating the enum wrapper.
PYBIND11_MODULE(so07, m)
{
auto enum_metaclass = create_enum_metaclass();
py::enum_<Kind>(m, "Kind", py::metaclass(enum_metaclass))
.value("Kind1", Kind::Kind1)
.value("Kind2", Kind::Kind2)
.value("Kind3", Kind::Kind3)
;
}
And now we can use it in Python:
>>> from so07 import Kind
>>> type(Kind)
<class 'importlib._bootstrap.pybind11_ext_enum'>
>>> len(Kind)
3
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论