调用一个独立的 C 程序(可执行文件)
这是最常见的情况,你已经有一个编译好的 C 程序(my_c_program),你想在 Python 脚本中执行它并获取其输出。
方法 1:使用 os.system() (最简单但不推荐)
这是最直接的方法,但它会将命令的控制权交给子 shell,无法获取程序的返回值或标准输出/错误流。
C 代码 (hello.c):
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
printf("Hello from C program!\n");
printf("Received %d arguments.\n", argc);
for (int i = 0; i < argc; i++) {
printf("Arg %d: %s\n", i, argv[i]);
}
sleep(2); // 模拟耗时操作
return 42; // 返回一个状态码
}
编译 C 程序:
gcc -o hello hello.c
Python 代码 (call_os_system.py):
import os
# 执行命令,无法获取输出或返回码
os.system("./hello")
# 可以传递参数
# os.system("./hello arg1 arg2")
缺点:
- 无法获取输出:
os.system执行后,你不知道 C 程序打印了什么。 - 无法获取返回码:无法知道 C 程序的
main函数返回的值(return 42)。 - 安全性问题:如果命令字符串来自不可信的输入,容易引发命令注入攻击。
- 交互性差:无法向 C 程程的标准输入发送数据。
方法 2:使用 subprocess 模块 (强烈推荐)
subprocess 是 Python 官方推荐的用于创建子进程的模块,功能强大且灵活,它替代了旧的 os.system, os.spawn*, popen2 和 commands 模块。
1 subprocess.run() (Python 3.5+ 推荐)
这是最现代、最推荐的接口。
Python 代码 (call_subprocess_run.py):
import subprocess
# --- 场景1: 执行命令并获取输出 ---
try:
# 执行命令,捕获标准输出和标准错误
# text=True 会自动将输出解码为字符串
# check=True 会在命令返回非零状态码时抛出 CalledProcessError 异常
result = subprocess.run(
["./hello", "arg1", "arg2"],
capture_output=True,
text=True,
check=True,
timeout=5 # 设置超时时间
)
# 打印 C 程序的返回码
print(f"Return code: {result.returncode}")
# 打印 C 程序的标准输出
print("Standard Output:")
print(result.stdout)
# check=True 且命令失败,这里可以打印标准错误
# 但如果 check=True 且命令成功,stderr 可能为空
if result.stderr:
print("Standard Error:")
print(result.stderr)
except subprocess.CalledProcessError as e:
print(f"Command failed with return code {e.returncode}")
print(f"Output: {e.output}")
print(f"Error: {e.stderr}")
except subprocess.TimeoutExpired as e:
print(f"Command timed out: {e.cmd}")
print(f"Output: {e.output}")
# --- 场景2: 不关心输出,只关心是否成功 ---
subprocess.run(["./hello"], check=True) # 如果失败,会抛出异常
print("Command executed successfully.")
# --- 场景3: 不想捕获输出,直接打印到终端 (类似 os.system) ---
# shell=True 可以使用 shell 特性,但要注意安全风险
subprocess.run("./hello arg1 arg2", shell=True)
优点:
- 功能全面:可以获取返回码、标准输出、标准错误。
- 安全:推荐使用列表形式传递参数 (
["./hello", "arg1"]),可以有效防止命令注入。 - 灵活:支持超时、工作目录、环境变量等高级选项。
- 代码清晰:
run()是一个同步调用,代码逻辑直观。
2 subprocess.Popen() (更底层,更灵活)
当你需要与子进程进行更复杂的交互时(持续读取输出、向其写入输入),Popen 是更好的选择,它创建一个进程对象,然后你可以手动操作它。
Python 代码 (call_subprocess_popen.py):
import subprocess
# 使用 Popen 启动进程
# stdout=subprocess.PIPE 表示捕获标准输出
# stderr=subprocess.PIPE 表示捕获标准错误
# text=True 表示以文本模式处理输入输出
proc = subprocess.Popen(
["./hello", "from_popen"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# wait() 等待进程结束
return_code = proc.wait()
# 获取输出
stdout, stderr = proc.communicate() # communicate() 会读取所有输出并等待进程结束
print(f"Popen Return code: {return_code}")
print("Popen Standard Output:")
print(stdout)
if stderr:
print("Popen Standard Error:")
print(stderr)
# --- 实时读取输出的例子 ---
print("\n--- Real-time output example ---")
proc = subprocess.Popen(["./hello", "realtime"], stdout=subprocess.PIPE, text=True, bufsize=1)
# 实时读取每一行
for line in proc.stdout:
print(f"[PYTHON] Received: {line.strip()}")
# 确保进程已经结束
proc.wait()
print(f"Realtime process finished with code: {proc.returncode}")
优点:
- 异步能力:可以启动进程后,不立即等待其结束,而是继续执行其他代码。
- 交互能力强:可以持续读写子进程的 I/O 流。
- 更底层控制:提供了
poll(),send_signal(),terminate(),kill()等方法来管理进程。
在 Python 中直接调用 C 函数 (无需独立程序)
如果你不想创建一个独立的可执行文件,而是想直接调用 C 代码中定义的函数,你需要使用 Python 的 C 扩展机制,这通常通过 ctypes 或 CFFI 库来实现。
方法 3:使用 ctypes 库 (无需编译)
ctypes 是 Python 的一个标准库,允许你调用动态链接库(.so 文件)中的 C 函数。
C 代码 (mylib.c):
#include <stdio.h>
// 一个简单的加法函数
int add(int a, int b) {
printf("C function 'add' called with %d and %d\n", a, b);
return a + b;
}
// 一个返回字符串的函数
const char* get_message() {
printf("C function 'get_message' called\n");
return "Hello from C library!";
}
编译成共享库 (.so 文件):
# -fPIC 生成位置无关代码,用于共享库 # -shared 指定生成共享库 gcc -fPIC -shared -o mylib.so mylib.c
Python 代码 (call_ctypes.py):
import ctypes
import os
# 加载共享库
# 使用绝对路径或确保 mylib.so 在当前目录下
lib_path = os.path.join(os.path.dirname(__file__), "mylib.so")
mylib = ctypes.CDLL(lib_path)
# --- 调用 add(int, int) 函数 ---
# 1. 告诉 Python C 函数的参数和返回类型
mylib.add.argtypes = [ctypes.c_int, ctypes.c_int]
mylib.add.restype = ctypes.c_int
# 2. 调用函数
result = mylib.add(10, 20)
print(f"Result from add(10, 20): {result}")
# --- 调用 get_message() 函数 ---
# 1. 告诉 Python C 函数的返回类型是 C 字符串
mylib.get_message.argtypes = [] # 无参数
mylib.get_message.restype = ctypes.c_char_p # 返回 C 风格的字符串
# 2. 调用函数
# ctypes 会自动将 C 字符串转换为 Python 字符串
c_string = mylib.get_message()
print(f"Result from get_message(): {c_string}")
# 记得释放 C 字符串占用的内存(如果需要)
# mylib.get_message.restype = ctypes.c_void_p # 如果返回的是 malloc 的内存
# c_string_ptr = mylib.get_message()
# print(f"Result: {ctypes.c_char_p(c_string_ptr).value}")
# ctypes.pythonapi.PyMem_Free(c_string_ptr)
优点:
- 高性能:直接调用 C 函数,没有进程创建和通信的开销。
- 无缝集成:C 函数可以像 Python 函数一样被调用。
- 无需修改 C 代码:只要能编译成
.so库就可以。
缺点:
- 类型管理:需要手动声明 C 的数据类型和 Python 数据类型的映射,容易出错。
- 内存管理:处理 C 分配的内存(如
malloc)需要小心,避免内存泄漏。 - 平台相关:
.so文件是 Linux 下的,Windows 下是.dll。
总结与选择
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
os.system |
简单、快速执行命令,不关心结果 | 极其简单 | 无法获取输出/返回码,不安全,交互性差 |
subprocess.run |
强烈推荐,执行外部程序并获取结果 | 功能强大,安全,代码清晰,推荐用于大多数情况 | 相比 os.system 代码稍多 |
subprocess.Popen |
需要高级进程控制(如实时交互、异步) | 灵活,交互能力强,功能最全面 | API 更复杂,代码量更多 |
ctypes |
需要直接调用 C 库中的函数,而不是执行整个程序 | 高性能,无缝集成,无需独立进程 | 类型管理复杂,内存管理需小心,平台相关 |
如何选择?
-
如果你的 C 代码是一个独立的命令行工具,并且你想在 Python 脚本中运行它:
- 首选
subprocess.run(),它提供了你需要的所有功能(获取输出、返回码、处理错误),并且是现代 Python 的标准方式。 - 如果需要实时读取输出或进行更复杂的交互,使用
subprocess.Popen()。
- 首选
-
如果你的 C 代码是一组函数,你想把它们作为 Python 的一部分来使用(为了计算密集型任务加速):
- 使用
ctypes,将 C 代码编译成.so库,然后在 Python 中加载并调用其中的函数,这是实现 Python 和 C 混合编程的高效方式。
- 使用
