Python技术迷

如何从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 调用这些导出函数。这一套下来虽然稍微绕点,但可读性强、部署灵活。

简单流程如下:

  1. 写一个 .pyx 文件,声明要暴露的函数:
# example.pyx
def add(int a, int b):
    return a + b
  1. 写 setup.py 编译成C扩展:
from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("example.pyx")
)
  1. 编译成 .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,全部免费领取!