本节摘要:Python 擅长胶水,Fortran 擅长引擎,这条"胶水与引擎"的分工已经是科学计算的主流架构。本节比较三条集成路线:f2py 自动封装、iso_c_binding 手写 C 桥、进程隔离与协议通信,各自给出适用场景与代价。读完你能为"把 Fortran 内核接进 Python 流水线"做出正确选型。
科研流水线长这样:Python 读数据、预处理、调度,把数据交给 Fortran 内核算,再把结果拿回来画图。Python 的开发效率与生态无可替代,Fortran 的数值性能不可替代,两者通过清晰的边界分工。这条路线之所以成立,是因为 Fortran 内核是"纯计算"的:输入数组、输出数组、无副作用——这样的模块最容易封装。如果你的内核动不动读写全局状态、依赖 Fortran 特有 I/O,集成成本会暴涨。
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 库 + 规整数组接口"。
复杂场景——对象句柄(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 路线用连续数组加指针传递实现近零拷贝。混合项目的性能瓶颈十有八九在数据搬移上,不在计算本身。
阅读完本节,你应当能够:
接进生态了,最后一章看怎么让整套代码活得更久——工程化与迁移。