Python 有很多的扩展,一般常用的是用 C/C++ , Python 编写,本文将介绍使用 Fortran 为Python编写扩展。

介绍

Fortran介绍

FORTRAN是英文“FORmulaTRANslator”的缩写,译为“公式翻译器”,它是世界上最早出现的计算机高级程序设计语言,广泛应用于科学和工程计算领域。FORTRAN语言以其特有的功能在数值、科学和工程计算领域发挥着重要作用。 - 摘抄自百度

Fortran 是一门古老的编程语言,1956年发明,比C语言更加原始。但是由于简单高效,在科学计算领域还有非常广泛的应用。

Mingw

MinGW,是Minimalist GNU for Windows的缩写。它是一个可自由使用和自由发布的Windows特定头文件和使用GNU工具集导入库的集合,允许你在GNU/Linux和Windows平台生成本地的Windows程序而不需要第三方C运行时(C Runtime)库。

混编的原理

由于Fortran非常古老且简单,所以Fortran编译产生的对象文件可以直接跟C语言的对象文件进行链接。通过这种方式,只需要按照python的C扩展方法,把fortran代码链接进去,即可完成pyd文件的生成。

案例

说明

编写一个最简单的Fortran加法函数,然后写一个C扩展,在C扩展中调用Fortran的函数,然后再由C扩展返回到Python。

代码

Fortran代码

一个简单的加法函数

1
2
3
4
5
6
7
8
9
10
11
12
module demomodule
contains
subroutine add(a,b,c)
integer,intent(in)::a
integer,intent(in)::b
integer,intent(out)::c
c=a+b
return

end subroutine add

end module demomodule

C扩展代码

这是一个最简单的C扩展例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "Python.h"
void __demomodule_MOD_add(int *a,int *b,int *c);
static PyObject *add_c(PyObject *self ,PyObject *args){

int a,b,c;
if (!PyArg_ParseTuple(args,"ii",&a,&b)){
return NULL;
}
__demomodule_MOD_add(&a,&b,&c);
return Py_BuildValue("i",c);
}
static PyMethodDef paliMethods[]={
{"add",add_c,METH_VARARGS,"add demo"},
{NULL,NULL,0,NULL}
};
static struct PyModuleDef add={
PyModuleDef_HEAD_INIT,
"add",
"my tools",
-1,
paliMethods
};
PyMODINIT_FUNC PyInit_add(void){
return PyModule_Create(&add);
}
  • 第2行,定义Fortran中的函数。此函数名称为Fortran的编译对象文件中的函数名称,可以使用mingw编译fortran代码,然后使用nm命令查看函数名称。
1
2
gfortran.exe -c add_f.f90 -o add_f.o
nm ./add_f.o

  • 第5~10行,接收传入的python参数,传入到Fortan函数中并返回,然后再返回到Python中
  • 其余行,简单的python的c扩展规则

setup.py

1
2
3
4
5
6
7
8
9
10
from setuptools import setup, Extension
module = Extension('add',
sources=['add_c.c', 'add_f.f90'], libraries=['gfortran'],
library_dirs=[r'D:\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64',],
extra_compile_args=['-O3'])
setup(
name="add",
version='1.3',
ext_modules=[module]
)

library_dirs 中填写的是ucrt库的路径,此库为 Windows 10 SDK的内容,此库为Python编写扩展的依赖,跟fortran没关系,感觉没啥用

extra_compile_args 中想加上优化,但是好像没成功

编译运行

环境准备

需要Mingw的环境,可以把mingw的路径加入到环境变量中,或者命令行执行如下命令,临时添加

1
set path=%PATH%;C:\msys64\mingw64\bin

编译代码

运行以下命令,编译生成库

1
python setup.py build --compiler=mingw32

然后出现以下错误。

此错误是因为Python不支持Fortran代码的编译。

修改如下:

使用Python3.6版本的,修改 Lib/distutils/cygwinccompiler.py 文件第 261 行。

1
2
3
4
5
6
for src_name in source_filenames:
# use normcase to make sure '.rc' is really '.rc' and not '.RC'
base, ext = os.path.splitext(os.path.normcase(src_name))
if ext not in (self.src_extensions + ['.rc','.res']): # 修改这一行
raise UnknownFileError("unknown file type '%s' (from '%s')" % \
(ext, src_name))

改为

1
2
3
4
5
6
for src_name in source_filenames:
# use normcase to make sure '.rc' is really '.rc' and not '.RC'
base, ext = os.path.splitext(os.path.normcase(src_name))
if ext not in (self.src_extensions + ['.rc','.res','.f90','.f']): # 添加对f90和f文件支持
raise UnknownFileError("unknown file type '%s' (from '%s')" % \
(ext, src_name))

重新运行以下命令,编译生成库

1
python setup.py build --compiler=mingw32

完成,如下图所示

安装库

运行以下命令,安装库

1
python setup.py install

安装完成,如下图所示

测试运行

使用如下代码

1
2
3
import add
c = add.add(66, 7)
print(c)

运行结果,如下所示

环境说明

以上代码的运行环境为

  • Python: 3.6.8
  • Mingw-gcc: gcc version 7.3.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)
  • Fortran 为自由格式

总结

缺少DLL或者链接库异常

解决: 把Mingw的路径加入到环境变量中。

其实只需要把那里面的dll文件拷贝到运行库目录中即可

Python3.8.10运行

修改代码的位置与Python3.6不同。

修改代码 Lib/site-packages/setuptools/_distutils/ccompiler.py 第966行

1
2
def out_extensions(self):
return dict.fromkeys(self.src_extensions, self.obj_extension)

改为

1
2
3
def out_extensions(self):
self.src_extensions.extend(['.f90','.f'])
return dict.fromkeys(self.src_extensions, self.obj_extension)

然后就可以编译了。

修改代码的原理是让gcc编译器识别fortan文件后缀,不同的python版本,不同的setuptools版本,修改的地方都不一样

可以按照以下的思路去修改:

  1. 找到 error: unknown file type '.f90' (from 'add_f.f90') 这条错误打印的位置.
  2. 尝试修改.

本节的环境:

  • python: 3.8.10
  • setuptools: 67.2.0

Linux

修改代码的位置与Python3.6不同。

修改代码 distutils/ccompiler.py 第854行

1
2
3
if ext not in self.src_extensions:
raise UnknownFileError(
"unknown file type '%s' (from '%s')" % (ext, src_name))

改为

1
2
3
if ext not in self.src_extensions+['.f90','.f']:
raise UnknownFileError(
"unknown file type '%s' (from '%s')" % (ext, src_name))

修改内容如图所示:

然后就可以编译了。

代码库

hufei/python_c_fortan