Skip to content

第4章 模块与函数导出

pybind11 的核心任务是在 C++ 和 Python 之间建立桥梁,而模块级函数是这种桥梁中最基本的元素。本章将详细介绍如何定义模块级函数、处理函数重载、配置参数行为,以及添加文档和类型信息。

pybind11 使用 PYBIND11_MODULE 宏来创建模块入口点,这是所有 pybind11 模块的标准写法。

example.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
// PYBIND11_MODULE 是 pybind11 创建模块的标准宏
// 第一个参数是模块名,第二个参数是模块对象的引用
PYBIND11_MODULE(example_module, m) {
// 模块级 docstring
m.doc() = "example_module - 演示模块级函数导出";
// 使用 m.def() 定义函数
// 语法: m.def("python_name", &cxx_function, "docstring");
m.def("add", [](int a, int b) { return a + b; }, "两个整数相加");
m.def("greet", [](const std::string& name) {
return "Hello, " + name + "!";
}, "返回问候语");
}
import example_module as em
result = em.add(1, 2) # 返回 3
print(em.greet("World")) # 输出: Hello, World!

PYBIND11_MODULE(example_module, m) 的工作原理:

  1. 模块名 (example_module): 这是 Python 中 import example_module 使用的名字
  2. 模块对象 (m): 这是一个 py::module_ 类型的引用,在宏内使用它来定义函数、类等
  3. 宏展开: pybind11 会生成适当的初始化代码,使模块能被 Python 导入

关键概念: m.def("name", callable, "doc") 的第三个参数是函数的 docstring,可以是多行字符串。pybind11 会将这些信息传递给 Python,使 help(funcname) 能正常工作。

当多个 C++ 函数具有相同的名称但不同的参数类型时,pybind11 可以通过函数重载来提供统一的 Python 接口。

overloads.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_MODULE(overloads, m) {
m.doc() = "函数重载演示";
// 方法1: 使用 lambda 直接定义多个重载
m.def("process", py::overload_cast<int>::dispatch([](int x) {
return x * 2;
}), "处理整数:加倍");
m.def("process", py::overload_cast<double>::dispatch([](double x) {
return x * 2.0;
}), "处理浮点数:加倍");
m.def("process", py::overload_cast<std::string>::dispatch([](const std::string& s) {
return s + s;
}), "处理字符串:重复一次");
// 方法2: 使用哑函数 + cast
// 定义重载的另一种方式
m.def("compute",
static_cast<int(*)(int, int)>(&compute_impl),
"计算两个整数的和");
m.def("compute",
static_cast<double(*)(double, double)>(&compute_impl),
"计算两个浮点数的和");
}
// 辅助函数(哑函数)
int compute_impl(int a, int b) { return a + b; }
double compute_impl(double a, double b) { return a + b; }
import overloads as ov
print(ov.process(5)) # 调用 int 版本,输出 10
print(ov.process(3.14)) # 调用 double 版本,输出 6.28
print(ov.process("Hi")) # 调用 string 版本,输出 "HiHi"

对于简单的重载,最常用的方式是利用 C++ 的类型推导:

PYBIND11_MODULE(overloads2, m) {
// 直接用 lambda 定义重载
m.def("compute", [](int a, int b) { return a + b; });
m.def("compute", [](double a, double b) { return a + b; });
m.def("compute", [](const std::string& a, const std::string& b) { return a + b; });
}

关键概念: py::overload_cast<Args>::dispatch() 是 pybind11 提供的类型安全重载分派机制。它消除了手动编写哑函数的需要,让代码更清晰。

C++ 的默认参数可以直接映射到 Python 的可选参数。

defaults.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_MODULE(defaults, m) {
m.doc() = "默认参数演示";
// 定义带默认参数的函数
// C++: void set_range(int min = 0, int max = 100)
// Python: set_range(min=0, max=100) 或 set_range(5) 或 set_range(5, 50)
m.def("set_range",
[](int min, int max) {
// 实际逻辑
return py::make_tuple(min, max);
},
py::arg("min") = 0,
py::arg("max") = 100,
"设置最小值和最大值,默认 min=0, max=100");
// 只有一个默认参数
m.def("set_name",
[](const std::string& name, bool uppercase) {
return uppercase ? py::str("{:>10}").format(name).cast<std::string>() : name;
},
py::arg("name"),
py::arg("uppercase") = false,
"设置名称,可选是否转换为大写");
}
import defaults as d
print(d.set_range()) # (0, 100)
print(d.set_range(10)) # (10, 100)
print(d.set_range(10, 50)) # (10, 50)
print(d.set_range(min=20)) # (20, 100)
print(d.set_range(max=200)) # (0, 200)
print(d.set_name("john")) # "john"
print(d.set_name("john", True)) # " JOHN"
m.def("func_name", callable,
py::arg("param_name") = default_value,
py::arg("other_param") = other_default,
"docstring");
  • py::arg("name") 指定参数名,这使得关键字参数在 Python 中可用
  • = default_value 指定默认值
  • 参数顺序必须与 C++ 函数签名一致

重要: C++ 默认参数必须与 pybind11 的 py::arg 声明顺序一致。如果 C++ 函数有默认参数,pybind11 会根据参数位置自动匹配。

Python 的 None 值在 C++ 中需要特殊处理。pybind11 使用 py::none 和 std::optional 来处理这类情况。

optional_args.cpp
#include <pybind11/pybind11.h>
#include <optional>
namespace py = pybind11;
PYBIND11_MODULE(optional_args, m) {
m.doc() = "可选参数处理演示";
// 方法1: 使用 std::optional 处理 None
m.def("find_user",
[](std::optional<int> user_id, std::optional<std::string> username) {
if (user_id.has_value()) {
return py::str("Found by ID: {}").format(user_id.value());
} else if (username.has_value()) {
return py::str("Found by name: {}").format(username.value());
}
return py::str("No criteria provided");
},
py::arg("user_id") = std::nullopt,
py::arg("username") = std::nullopt,
"根据 ID 或用户名查找用户,两者都为 None 时返回提示");
// 方法2: 使用 py::none 处理可选参数
m.def("process_data",
[](const std::string& data, py::none callback) {
if (callback.is_none()) {
return "Data: " + data + " (no callback)";
}
return "Data: " + data + " (callback provided)";
},
py::arg("data"),
py::arg("callback") = py::none(),
"处理数据,callback 为 None 时跳过回调");
}
import optional_args as oa
print(oa.find_user(None, "Alice")) # "Found by name: Alice"
print(oa.find_user(42, None)) # "Found by ID: 42"
print(oa.find_user(None, None)) # "No criteria provided"
print(oa.find_user(user_id=42)) # "Found by ID: 42"
print(oa.find_user(username="Bob")) # "Found by name: Bob"
print(oa.process_data("test")) # "Data: test (no callback)"
print(oa.process_data("test", None)) # "Data: test (no callback)"
// 当 Python 传递 None 时,C++ 端接收到的实际值
py::none x; // py::none 类型对象
x.is_none(); // true
std::optional<T> y; // 可选类型,None 时 has_value() == false
C++ 类型Python 传 None 时的行为
py::none接收 py::none 对象,需调用 .is_none() 检查
std::optional<T>接收 std::nullopt,has_value() 返回 false
T* (指针)接收 nullptr

使用 py::kwarg 可以让函数接受任意关键字参数,这在实现配置函数时非常有用。

kwargs.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_MODULE(kwargs, m) {
m.doc() = "关键字参数演示";
// 定义一个接受 kwargs 的函数
m.def("configure",
[](py::kwargs kwargs) {
py::dict result;
// 遍历所有传入的关键字参数
for (auto [key, value] : kwargs) {
std::string key_str = py::str(key).cast<std::string>();
py::str val_str = py::str(value);
result[key_str] = val_str;
}
return result;
},
"接受任意关键字参数并返回字典");
}
import kwargs as kw
config = kw.configure(host="localhost", port=8080, debug=True)
print(config) # {'host': 'localhost', 'port': 8080, 'debug': True}
empty = kw.configure()
print(empty) # {}

py::kwargs 特别适合:

  • 配置函数:接受多个可选配置项
  • 转发函数:将 kwargs 传递给其他函数
  • 插件系统:允许动态扩展参数

注意: py::kwargs 参数必须是函数的最后一个参数。

pybind11 允许限制函数参数只能通过位置或关键字传递。

arg_restrictions.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_MODULE(arg_restrictions, m) {
m.doc() = "位置参数限制演示";
// 位置参数模式(默认)
// Python 调用: func(a, b, c) - 三个都是位置参数
m.def("positional_only",
[](int a, int b, int c) {
return a + b + c;
},
py::arg("a"),
py::arg("b"),
py::arg("c"),
"所有参数只能是位置参数(默认行为)");
// 使用 py::kw_only() 标记关键字参数分隔符
// 在分隔符之前的参数可以使用位置或关键字传递
// 在分隔符之后的参数只能使用关键字传递
m.def("keyword_only",
[](int a, int b, int c) {
return a + b + c;
},
py::arg("a"), // 可以用位置或关键字
py::arg("b"), // 可以用位置或关键字
py::kw_only(), // 分隔符:之后都是关键字-only
py::arg("c"), // 只能使用关键字传递
"a 和 b 可以用位置或关键字,c 只能关键字");
}
import arg_restrictions as ar
print(ar.positional_only(1, 2, 3)) # 6
print(ar.positional_only(1, 2, c=3)) # 6
print(ar.positional_only(a=1, b=2, c=3)) # 6
print(ar.keyword_only(1, 2, c=3)) # 6
print(ar.keyword_only(1, b=2, c=3)) # 6
模式何时使用
默认(无限制)普通函数,任一参数都可用关键字
py::kw_only()强制某些参数必须使用关键字(如配置参数)
Python 3.8+ 的 / 标记在 Python 端用 def f(a, /, b) 表示位置参数

好的文档对可维护性至关重要。pybind11 支持为模块、函数和参数提供文档字符串。

docstrings.cpp
#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_MODULE(docstrings, m) {
// 模块级 docstring
m.doc() = R"pbdoc(
docstrings - 演示文档字符串
==============================
这个模块展示了 pybind11 文档字符串的各种用法。
示例:
import docstrings
help(docstrings.add) # 查看函数帮助
)pbdoc";
// 函数 docstring
m.def("add",
[](int a, int b) { return a + b; },
R"pbdoc(
将两个数字相加
Args:
a (int): 第一个数字
b (int): 第二个数字
Returns:
int: 两个数字的和
Raises:
无
Example:
>>> add(1, 2)
3
)pbdoc",
py::arg("a"),
py::arg("b"));
// 类 docstring(下一章会讲)
// py::class_<MyClass>(m, "MyClass")
// .def(py::init<>())
// .doc() = "MyClass 的文档...";
}
import docstrings as ds
print(ds.add.__doc__)
help(ds.add)
// 推荐:使用原始字符串常量(R"pbdoc(...)pbdoc")
// 这样可以包含换行和引号而不需要转义
m.def("complex_func",
[](int x, const std::string& s, double d) { /* ... */ },
R"pbdoc(
复杂函数的文档
这个函数执行以下操作:
1. 验证输入
2. 处理数据
3. 返回结果
Args:
x: 整数值,范围 [0, 100]
s: 字符串参数,不能为空
d: 浮点数,默认 1.0
Returns:
处理后的结果字符串
Note:
这个函数线程安全
)pbdoc",
py::arg("x"),
py::arg("s"),
py::arg("d") = 1.0);

Python 的函数签名 introspection 允许 IDE 和工具提供自动补全和参数提示。

signature.cpp
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
PYBIND11_MODULE(signature, m) {
m.doc() = "函数签名演示";
// 方式1: 隐式签名(默认)
m.def("implicit_sig",
[](int a, double b, const std::string& c) { return 0; },
"隐式签名:参数名自动从 py::arg 获取");
// 方式2: 显式签名(推荐)
m.def("explicit_sig",
[](int a, double b, const std::string& c) { return 0; },
R"pbdoc(
显式签名:使用 py::keep_alive 演示复杂签名
Args:
a: 第一个参数
b: 第二个参数
c: 第三个参数
Returns:
整数结果
)pbdoc",
py::arg("a"),
py::arg("b"),
py::arg("c"));
// 签名与类型提示(Python 3.8+)
m.def("typed_hint",
[](int x) -> int { return x * 2; },
py::arg("x"),
"带类型提示的函数");
// 多重重载签名
m.def("overloaded_sig",
py::overload_cast<int, int>(&sum_impl),
py::arg("a"),
py::arg("b"),
"两个整数求和");
m.def("overloaded_sig",
py::overload_cast<double, double>(&sum_impl),
py::arg("a"),
py::arg("b"),
"两个浮点数求和");
}
// 辅助函数
int sum_impl(int a, int b) { return a + b; }
double sum_impl(double a, double b) { return a + b; }
import signature as sig
import inspect
s = inspect.signature(sig.explicit_sig)
print(s) # (a: int, b: float, c: str)
for name, param in s.parameters.items():
print(f" {name}: default={param.default}, annotation={param.annotation}")
print(sig.explicit_sig.__doc__)

pybind11 会自动为函数生成 __annotations__,Python IDE 可以利用这些信息提供类型提示:

@decorator
def wrapper(f):
# 通过 inspect.signature(f) 可以获取原始函数的参数类型
pass

实用技巧: 配合 py::signature() 可以自定义函数签名显示。在大多数情况下,让 pybind11 自动处理即可。

本章介绍了 pybind11 模块和函数导出的核心概念:

主题关键点
模块定义PYBIND11_MODULE(name, m) 是标准入口点
函数定义m.def("py_name", &cxx_func, "doc")
函数重载py::overload_cast<Args>::dispatch()
默认参数py::arg("name") = default_value
可选参数std::optional<T> 和 py::none
关键字参数py::kwargs 捕获所有关键字参数
参数限制py::kw_only() 标记关键字-only 参数
文档字符串使用 R"pbdoc(...)pbdoc" 原始字符串
函数签名自动生成,可通过 inspect.signature() 查看

下一章我们将学习如何导出 C++ 类到 Python,包括构造函数、成员函数、继承等高级特性。