Skip to content

第9章 容器类型转换

STL 容器与 Python 内置数据结构之间的转换是日常开发中最常见的场景。本章详细讲解 vector、map、set 等容器与 Python list、dict、set 之间的映射机制。

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)。

默认情况下,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 端失去自动类型转换的便利。

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。

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}
std::unordered_map<std::string, double> scores;

自动转换为 Python dict,但顺序不确定(因为 unordered_map 不保证迭代顺序)。

#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}

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", &divide);
}
>>> example.divide(10, 3)
(3, 'not divisible')
>>> example.divide(9, 3)
(3, 'divisible')

复杂的嵌套结构也能自动转换:

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;

对于过深的嵌套,编译时间会显著增加。

理解 pybind11 何时复制数据非常重要:

场景行为
参数传入Python → C++ 时,基础类型会复制;大容器默认也是复制
返回值C++ → Python 时,返回值会被移动或复制到 Python 对象
// 这个函数接收 vector 参数
void process(std::vector<int> data) {
// data 是 C++ vector,Python 传来的 list 已转换为 vector
// 修改 data 不会影响 Python 端的原对象
}
// 返回 vector 时,返回的是新的 Python list
std::vector<int> generate() {
return {1, 2, 3}; // 移动语义,返回给 Python
}

如果你希望 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++ 端,需要谨慎管理生命周期。

对于需要底层内存访问的场景,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 类型(容器级)。选择取决于你的性能需求和数据访问模式。