第9章 容器类型转换
STL 容器与 Python 内置数据结构之间的转换是日常开发中最常见的场景。本章详细讲解 vector、map、set 等容器与 Python list、dict、set 之间的映射机制。
9.1 std::vector ↔ Python list
Section titled “9.1 std::vector ↔ Python list”std::vector 是最常用的容器。pybind11 默认按值语义进行自动转换:
#include <pybind11/pybind11.h>#include <vector>
namespace py = pybind11;
std::vector<int> sum_vectors(std::vector<int> a, std::vector<int> b) { std::vector<int> result(a.size()); for (size_t i = 0; i < a.size(); ++i) { result[i] = a[i] + b[i]; } return result;}
PYBIND11_MODULE(example, m) { m.def("sum_vectors", &sum_vectors);}>>> import example>>> a = [1, 2, 3]>>> b = [4, 5, 6]>>> example.sum_vectors(a, b)[5, 7, 9]对于 int、float 等基础类型,pybind11 使用特殊的 type_caster 进行高效复制。但对于自定义类型或复杂结构,我们需要使用透明类型(opaque type)。
9.2 opaque 类型
Section titled “9.2 opaque 类型”默认情况下,pybind11 会递归转换 vector 内的元素。但如果 vector 包含复杂的自定义类型,而你希望 Python 直接操作这个 vector(不经过逐元素转换),可以使用 PYBIND11_MAKE_OPAQUE:
#include <pybind11/pybind11.h>#include <pybind11/stl.h>#include <vector>
namespace py = pybind11;
// 首先声明某个复杂类型struct MyData { std::string name; std::vector<double> values;};
// 告诉 pybind11 这个类型不需自动转换PYBIND11_MAKE_OPAQUE(std::vector<MyData>);
struct Config { std::vector<MyData> items;};现在,如果不使用 opaque,pybind11 会尝试将每个 MyData 转换为 Python 对象,这可能导致编译失败或运行时不正确。使用 opaque 后,你需要显式绑定:
py::bind_vector<std::vector<MyData>>(m, "MyDataVector");关键洞察:opaque 类型的目的是告诉 pybind11「不要递归转换这个容器」。当你需要 Python 直接持有一个 C++ 容器的引用(如修改后 C++ 端能看见变化),opaque 是正确选择。但代价是 Python 端失去自动类型转换的便利。
9.3 std::array ↔ Python tuple
Section titled “9.3 std::array ↔ Python tuple”std::array 转换为 Python tuple:
#include <pybind11/pybind11.h>#include <array>
namespace py = pybind11;
std::array<int, 3> cross_product(std::array<int, 3> a, std::array<int, 3> b) { return { a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0] };}
PYBIND11_MODULE(example, m) { m.def("cross_product", &cross_product);}>>> import example>>> a = (1, 0, 0)>>> b = (0, 1, 0)>>> example.cross_product(a, b)(0, 0, 1)关键洞察:tuple 是固定大小的,这与
std::array的固定大小特性匹配。但 Python tuple 不可修改,所以 C++ 端的返回值会创建新的 tuple,而非引用现有 array。
9.4 std::map ↔ Python dict
Section titled “9.4 std::map ↔ Python dict”std::map<K, V> 自动转换为 Python dict:
#include <pybind11/pybind11.h>#include <map>#include <string>
namespace py = pybind11;
std::map<std::string, int> count_words(const std::vector<std::string>& words) { std::map<std::string, int> counts; for (const auto& word : words) { counts[word]++; } return counts;}
PYBIND11_MODULE(example, m) { m.def("count_words", &count_words);}>>> example.count_words(["apple", "banana", "apple", "cherry", "banana", "apple"]){'apple': 3, 'banana': 2, 'cherry': 1}unordered_map 同样支持
Section titled “unordered_map 同样支持”std::unordered_map<std::string, double> scores;自动转换为 Python dict,但顺序不确定(因为 unordered_map 不保证迭代顺序)。
9.5 std::set ↔ Python set
Section titled “9.5 std::set ↔ Python set”#include <pybind11/pybind11.h>#include <set>
namespace py = pybind11;
std::set<int> intersection(std::set<int> a, std::set<int> b) { std::set<int> result; for (int x : a) { if (b.count(x)) result.insert(x); } return result;}
PYBIND11_MODULE(example, m) { m.def("intersection", &intersection);}>>> example.intersection({1, 2, 3, 4}, {3, 4, 5, 6}){3, 4}9.6 std::pair ↔ Python tuple
Section titled “9.6 std::pair ↔ Python tuple”std::pair<A, B> 转换为两个元素的 Python tuple:
#include <pybind11/pybind11.h>#include <utility>
namespace py = pybind11;
std::pair<int, std::string> divide(int a, int b) { return {a / b, a % b == 0 ? "divisible" : "not divisible"};}
PYBIND11_MODULE(example, m) { m.def("divide", ÷);}>>> example.divide(10, 3)(3, 'not divisible')>>> example.divide(9, 3)(3, 'divisible')9.7 嵌套容器
Section titled “9.7 嵌套容器”复杂的嵌套结构也能自动转换:
std::map<std::string, std::vector<int>> nested;>>> nested = {'a': [1, 2], 'b': [3, 4]}>>> # 双向自动转换std::vector<std::map<std::string, std::vector<std::pair<int, double>>>> complex_nested;对于过深的嵌套,编译时间会显著增加。
9.8 复制语义 vs 引用语义
Section titled “9.8 复制语义 vs 引用语义”理解 pybind11 何时复制数据非常重要:
| 场景 | 行为 |
|---|---|
| 参数传入 | Python → C++ 时,基础类型会复制;大容器默认也是复制 |
| 返回值 | C++ → Python 时,返回值会被移动或复制到 Python 对象 |
// 这个函数接收 vector 参数void process(std::vector<int> data) { // data 是 C++ vector,Python 传来的 list 已转换为 vector // 修改 data 不会影响 Python 端的原对象}
// 返回 vector 时,返回的是新的 Python liststd::vector<int> generate() { return {1, 2, 3}; // 移动语义,返回给 Python}获取引用语义
Section titled “获取引用语义”如果你希望 Python 端直接操作 C++ 数据(零复制),需要使用 py::return_value_policy::take_ownership 或 py::return_value_policy::reference:
class Buffer {public: std::vector<float> data;};
py::class_<Buffer>(m, "Buffer") .def(py::init<>()) .def_readwrite("data", &Buffer::data); // 直接引用 C++ 的 vector关键洞察:默认情况下 pybind11 使用「复制」策略(
return_value_policy::automatic),这意味着性能开销与容器大小成正比。对于高频调用的大数据场景,考虑使用 opaque 类型或显式的引用策略。但要注意:引用语义意味着 Python 端的修改会直接影响 C++ 端,需要谨慎管理生命周期。
py::buffer 协议
Section titled “py::buffer 协议”对于需要底层内存访问的场景,pybind11 支持 Python 的 buffer 协议:
#include <pybind11/pybind11.h>#include <pybind11/buffer_info.h>
struct Matrix { int rows, cols; std::vector<float> data;};
py::class_<Matrix>(m, "Matrix") .def(py::init<int, int>()) .def_buffer([](Matrix& m) -> py::buffer_info { return py::buffer_info( m.data.data(), // 指针 sizeof(float), // 单元素大小 py::format_descriptor<float>::format(), // 格式 2, // 维度 {m.rows, m.cols}, // shape {m.cols * sizeof(float), sizeof(float)} // strides ); }) .def_readwrite("rows", &Matrix::rows) .def_readwrite("cols", &Matrix::cols);>>> import numpy as np>>> m = Matrix(3, 4)>>> arr = np.asarray(m) # 不复制,直接引用!关键洞察:buffer 协议允许 Python 高效访问 C++ 内存,无需复制。当与 NumPy 配合使用时,这实现真正的零拷贝传输。但必须确保内存布局符合 Python 的期望( strides、contiguity 等)。
| 容器类型 | Python 类型 | 转换方式 |
|---|---|---|
std::vector<T> | list | 自动(递归转换元素) |
std::array<T, N> | tuple | 自动 |
std::map<K, V> | dict | 自动 |
std::unordered_map<K, V> | dict | 自动(顺序不确定) |
std::set<T> | set | 自动 |
std::pair<A, B> | tuple | 自动 |
关键洞察:pybind11 使用
pybind11/stl.h中的重载自动处理 STL 容器转换。对于简单类型(int、float、string),转换高效且透明。对于复杂类型,你需要决定是使用自动转换(元素级)还是 opaque 类型(容器级)。选择取决于你的性能需求和数据访问模式。