如何从C访问用Python编写的模块?
在实际开发或者集成过程中,经常会遇到一些“看起来不可能”的需求,比如:公司有一个核心算法模块是用Python写的(可能是某个数据科学家搞的),但整个系统的主干却是C语言的,这时候怎么整?🤔
不可能叫那位Python哥把代码重写一遍C吧……这锅不能背。所以面试里问“如何从C访问用Python编写的模块”,其实就是在考你对语言互操作性的理解深度,甚至是对“工程hack能力”的认知。
这事儿说难也难,说简单也简单。常规解法一般有三种路线:用Python官方提供的C API、搞个嵌入式解释器,或者走轮子路线比如Pybind11、Cython这类“半自动化桥接工具”。
我先说最原始也最有代表性的一种:Python C API。
这是最硬核、最贴近底层的一种方式,适合喜欢“手撸指针”的硬核C语言选手。但说实话,这玩意儿上手门槛高得吓人。你得先包含 <Python.h>,然后一堆 PyObject 指针的操作,什么 PyImport_ImportModule、PyObject_GetAttrString、PyTuple_New,写起来又臭又长。
简单示范一段吧,比如我们要在C代码里调用Python模块 math 下的 sqrt 函数来开根号:
#include<Python.h>
intmain(){
Py_Initialize(); // 初始化Python解释器
PyObject *pModule = PyImport_ImportModule("math");
if (pModule != NULL) {
PyObject *pFunc = PyObject_GetAttrString(pModule, "sqrt");
if (pFunc && PyCallable_Check(pFunc)) {
PyObject *pArgs = PyTuple_Pack(1, PyFloat_FromDouble(9.0));
PyObject *pValue = PyObject_CallObject(pFunc, pArgs);
if (pValue != NULL) {
printf("sqrt(9) = %f\n", PyFloat_AsDouble(pValue));
Py_DECREF(pValue);
}
Py_DECREF(pArgs);
}
Py_XDECREF(pFunc);
Py_DECREF(pModule);
}
Py_Finalize(); // 关闭解释器
return0;
}
没错,就是这么丑。你必须自己管理内存,自己检查NULL,一不小心就内存泄漏,还不如用C直接算个 sqrt(9) 来得快🙃
当然,正是因为这个API太反人类了,所以社区搞出了各种轮子来解放程序员双手。
路线二:Pybind11
如果你用的是C++,强烈建议试试Pybind11。它本质上是一个header-only的库,帮你把C++类或者函数“暴露”成Python模块,也可以反过来在C++中用Python的东西。
虽然Pybind11主要是为了C++嵌入Python而生的,但它也支持反向调用:从C++调用Python代码。这比用Python C API要优雅得多。
Pybind11大致用法如下:
#include<pybind11/embed.h> // 需要 C++11
namespace py = pybind11;
intmain(){
py::scoped_interpreter guard{}; // 初始化Python解释器
py::object math = py::module_::import("math");
double result = math.attr("sqrt")(9.0).cast<double>();
std::cout << "sqrt(9) = " << result << std::endl;
}
是不是干净利落多了?自动内存管理,还能链式调用,写着都舒坦。
不过Pybind11这条路不适合纯C项目,你得至少是个支持C++11的环境才行。
路线三:搞Cython当桥梁
Cython更像是一种“语言胶水工具”,你可以用它把Python代码编译成C扩展,然后用C语言通过 .so 或 .dll 调用这些导出函数。这一套下来虽然稍微绕点,但可读性强、部署灵活。
简单流程如下:
写一个 .pyx文件,声明要暴露的函数:
# example.pyx
def add(int a, int b):
return a + b
写 setup.py编译成C扩展:
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("example.pyx")
)
编译成 .so或.dll,然后在C里用dlopen或LoadLibrary加载并调用。
不过说实话,这套“用C调用Cython编译出来的Python函数”,常见于跨语言SDK开发或者高性能模块集成。普通业务开发不建议走这条,维护成本高,谁接盘谁头秃。
还有个特殊用途场景是嵌入Python解释器做脚本扩展系统,比如很多游戏引擎、图形引擎,C写的主干,Python作为可扩展脚本语言,这种情况下就会大量嵌入Python解释器来运行外部脚本。
哦对了,说个坑,Python路径和模块加载路径在这种嵌入式场景下经常出问题。比如你从C里调用Python模块,结果导入失败,多半是 PYTHONPATH 环境变量没设对,或者解释器初始化后找不到模块文件。面试要是聊到这块,你可以顺带一提:这事我踩过。
再进阶一点的做法,比如用 SWIG 生成绑定代码,把Python模块编译成C调用接口,也能搞,但SWIG文档堪比天书,不是特别推荐。
最后总结一下吧:
想直接干,Python C API够用了,但写起来糙; 想优雅干,Pybind11是首选,缺点是只能用在C++; 想工程自动化,Cython最合适,尤其适合模块复用; 想省事?不如直接把Python程序拆成HTTP服务让C来调用🙃——虽然土,但真香
说实话,现在这年头,语言互操作早就不是难题,难的是你愿不愿意花时间调坑。面试问这个题其实也不是在考你会不会,而是看你有没有“把系统搞起来”的那种动手能力。
所以你要是能一边讲清楚技术方案,一边还能穿插点踩坑经历,比如“我有次在用C调Python的时候因为Py_Finalize()之后再调用解释器直接段错误”,那HR多半会眼前一亮。
讲真,要真能讲出这些细节,你就是他们要的那种“能把锅背好还能继续往前走”的人🫡
话说回来,如果哪天你也准备从C调Rust模块,那故事可就更精彩了😏
最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:https://www.songshuhezi.com/deepseek
对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,全部免费领取!