7.3 与Python等脚本语言集成


7.3 与Python等脚本语言集成

本节摘要:Python 擅长胶水,Fortran 擅长引擎,这条"胶水与引擎"的分工已经是科学计算的主流架构。本节比较三条集成路线:f2py 自动封装、iso_c_binding 手写 C 桥、进程隔离与协议通信,各自给出适用场景与代价。读完你能为"把 Fortran 内核接进 Python 流水线"做出正确选型。

为什么是 Python 当胶水

科研流水线长这样:Python 读数据、预处理、调度,把数据交给 Fortran 内核算,再把结果拿回来画图。Python 的开发效率与生态无可替代,Fortran 的数值性能不可替代,两者通过清晰的边界分工。这条路线之所以成立,是因为 Fortran 内核是"纯计算"的:输入数组、输出数组、无副作用——这样的模块最容易封装。如果你的内核动不动读写全局状态、依赖 Fortran 特有 I/O,集成成本会暴涨。

路线一:f2py 自动生成封装

f2py 是 NumPy 自带的 Fortran 封装工具:给 Fortran 源码加少量注释说明意图,f2py 自动生成 Python 可调的扩展模块,数组按 NumPy 数组传入传出。上手最快,适合"内核就是几个纯函数、数组接口规整"的项目。

!> 向量加标量:y = a * x + y,供 f2py 封装 subroutine axpy(n, a, x, y) use iso_fortran_env, only: real64 implicit none integer, intent(in) :: n real(real64), intent(in) :: a real(real64), intent(in) :: x(n) real(real64), intent(inout) :: y(n) integer :: i do i = 1, n y(i) = a * x(i) + y(i) end do end subroutine axpy
# Python 侧调用封装好的 Fortran 模块 import numpy as np import solver_mod # f2py 生成的扩展模块 x = np.ones(1000) y = np.zeros(1000) solver_mod.axpy(2.0, x, y) # 数组自动映射,inplace 修改 y print(y[:5])

f2py 的边界:它自动生成的是"按约定调用"的桥,遇到派生类型、指针、复杂句柄就要手动处理或退回下一条路线。它最适合的形态是"纯 Fortran 库 + 规整数组接口"。

路线二:iso_c_binding 手写 C 桥

复杂场景——对象句柄(7.2 节)、多态、需要精细控制数据拷贝——用 iso_c_binding 把 Fortran 导出成 C 符号,再从 Python 侧通过 ctypes 或 cffi 调用。这条路线工作量比 f2py 大,但边界完全可控。

module solver_iface_mod use iso_c_binding, only: c_ptr, c_loc, c_f_pointer, c_double use iso_fortran_env, only: real64 implicit none contains ! 供 Python 调用的句柄式接口 function solver_create() result(h) bind(c, name="solver_create") type(c_ptr) :: h real(real64), pointer :: p allocate(p) p = 0.0_real64 h = c_loc(p) end function solver_create subroutine solver_advance(h, x, n) bind(c, name="solver_advance") type(c_ptr), value :: h integer, value :: n real(c_double) :: x(n) real(real64), pointer :: p call c_f_pointer(h, p) x = x + p end subroutine solver_advance end module solver_iface_mod
# Python 侧用 ctypes 调用手写桥 import ctypes as ct from numpy.ctypeslib import ndpointer lib = ct.CDLL('solver_core') # 加载 Fortran 编译产物的共享库 lib.solver_create.restype = ct.c_void_p lib.solver_advance.argtypes = [ ct.c_void_p, ct.c_int, ndpointer(ct.c_double, flags='C_CONTIGUOUS') ] h = lib.solver_create() x = np.zeros(10) lib.solver_advance(h, 10, x)

注意 argtypes 里声明了 C 连续数组——ctypes 要求 NumPy 数组内存连续(第 3 章的 contiguous 在这里直接对应),不连续就报错或静默错位。这条路线把你从 f2py 的约定中解放出来,代价是每加一个函数都要写接口声明,且符号签名必须与 C 侧逐一对应。

路线三:进程隔离与协议

当 Fortran 代码改造成本太高(遗留系统、编译环境特殊),或两边都需要独立运行时,用进程隔离:Fortran 程序独立运行,通过标准输入输出、文件或消息协议与 Python 通信。简单场景用文本协议或二进制文件交换;高性能场景用零拷贝共享内存。这条路线绕开了所有 ABI 问题,但每次交互都要序列化与进程启动开销,适合"低频、大块、改造贵"的集成。

三条路线怎么选

判断顺序就三问。一问内核形态:纯数组函数库,直接上 f2py;带对象句柄或复杂结构,走手写 C 桥。二问交互频率:每步都要交换,用进程内(前两条路线);低频大块交换,进程隔离也能接受。三问改造预算:Fortran 侧动不了,进程隔离兜底。记住一条铁律:数据尽量少拷贝。f2py 默认会按需拷贝以保证安全,明确标记 intent 可以减少拷贝;ctypes 路线用连续数组加指针传递实现近零拷贝。混合项目的性能瓶颈十有八九在数据搬移上,不在计算本身。

学习目标

阅读完本节,你应当能够:

  1. 说出 f2py 的封装流程与适用条件
  2. 说明手写 C 桥路线为什么在复杂场景更可控
  3. 解释进程隔离路线适合什么样的集成
  4. 判断一个混合项目该选哪条路线,并给出理由

本节要点回顾

  • 胶水与引擎是主流架构:Python 管流程,Fortran 算内核,边界清晰才能集成便宜
  • f2py 最快但有边界:纯函数加规整数组接口最合适
  • 手写 C 桥最可控:对象句柄、精细数据控制都在这里,ctypes 要求连续数组
  • 进程隔离兜底:改造贵、频率低、需要独立运行时的时候用
  • 选型三问:内核形态、交互频率、改造预算
  • 数据拷贝是性能大头:intent 声明与连续数组是减少拷贝的两把钥匙

接进生态了,最后一章看怎么让整套代码活得更久——工程化与迁移。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U