Link f2py 使用 setuptools 在 python 包中生成 *.so 文件

Link f2py generated *.so file in a python package using setuptools

我希望使用安装工具将包部署到 PyPi。然而,包的核心部分实际上是用 Fortran 编写的,我正在使用 f2py 将其包装在 python 中。项目的结构基本上是这样的:

my_project

模块 myfunc.py 导入 hello.so (import my_project.hello),然后可以由 myfunc.py 中的函数使用。这在我的机器上完美运行。

然后我尝试了标准的 setuptools 安装:sudo python3 setup.py install 在我的 Ubuntu 上,它安装得很完美。但不幸的是,在导入时,它会抛出 ModuleNotFoundError: No module named 'hello'.

现在,据我了解,在基于 Linux 的系统上,对于 python,共享库 *.so 存储在 /usr/lib/python3/dist-packages/ 中。所以我手动将这个 hello.so 复制到那里,我得到了一个工作包!但当然这只适用于本地。我想做的是告诉 setuptools 在 python-egg 中包含 hello.so 并自动进行复制等,这样当用户使用 pip3 install my_package 时,他们就可以访问这个自动共享库。我可以看到 numpy 以某种方式实现了这一点,但即使在查看了他们的代码之后,我也无法解码他们是如何做到的。有人可以帮我弄这个吗?提前致谢。

你可以用这样的setup.py文件来实现(简化版本,只保留构建外部模块的相关部分)

import os
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext


class f2py_Extension(Extension):

    def __init__(self, name, sourcedirs):
        Extension.__init__(self, name, sources=[])
        self.sourcedirs = [os.path.abspath(sourcedir) for sourcedir in sourcedirs]
        self.dirs = sourcedirs

class f2py_Build(build_ext):

    def run(self):
        for ext in self.extensions:
            self.build_extension(ext)

    def build_extension(self, ext):
        # compile
        for ind,to_compile in enumerate(ext.sourcedirs):
            module_loc = os.path.split(ext.dirs[ind])[0]
            module_name = os.path.split(to_compile)[1].split('.')[0]
            os.system('cd %s;f2py -c %s -m %s' % (module_loc,to_compile,module_name))

setup(
    name="foo",
    ext_modules=[f2py_Extension('fortran_external',['foo/one.F90','foo/bar/two.F90'])],
    cmdclass=dict(build_ext=f2py_Build),
)

构建外部模块的必要部分是 setup(...) 中的 ext_modulescmdclassext_modules只是一个Extension实例的列表,每个Extension实例描述了一组扩展模块。在上面的 setup.py 中,我告诉 ext_modules 我想用两个源文件 foo/test.F90foo/bar/two.F90 创建两个外部模块。基于ext_modulescmdclass负责编译这两个模块,在我们的例子中,编译模块的命令是

'cd %s;f2py -c %s -m %s' % (module_loc,to_compile,module_name)

安装前的项目结构

├── foo
│   ├── __init__.py
│   ├── bar
│   │   └── two.F90
│   └── one.F90
└── setup.py

python setup.py install

之后的项目结构
├── build
│   └── bdist.linux-x86_64
├── dist
│   └── foo-0.0.0-py3.7-linux-x86_64.egg
├── foo
│   ├── __init__.py
│   ├── __pycache__
│   │   └── __init__.cpython-37.pyc
│   ├── bar
│   │   ├── two.F90
│   │   └── two.cpython-37m-x86_64-linux-gnu.so
│   ├── one.F90
│   └── one.cpython-37m-x86_64-linux-gnu.so
├── foo.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   └── top_level.txt
└── setup.py

one.F90two.F90这两个源文件很简单

one.F90

module test

  implicit none

  contains

  subroutine add(a)

    implicit none
    integer :: a
    integer :: b
    b = a + 1
    print *, 'one',b

  end subroutine add


end module test

two.F90

module test

  implicit none

  contains

  subroutine add(a)

    implicit none
    integer :: a
    integer :: b
    b = a + 2
    print *, 'two',b

  end subroutine add


end module test

我安装包后,可以成功运行

>>> from foo.bar.two import test
>>> test.add(5)
 two           7

>>> from foo.one import test
>>> test.add(5)
 one           6

这是一种基于 F2PY's documentation 的方法(那里的示例包括构建多个 F2PY 模块,每个模块有多个源文件),利用 numpy.distutils,支持 Fortran 源文件。

具有多个 F2PY 扩展模块的最小示例的结构基于 src directory layout。它不是necessary/required,但优点是测试例程不能运行,除非包已经安装成功。

源布局

my_project
|
+-- src
|   |
|   +-- my_project
|       |
|       +-- __init__.py
|       +-- mod1.py
|       +-- funcs_m.f90
|       +-- two
|           |
|           +-- pluss2.f90
|           +-- times2.f90
|
+-- test_my_project.py
+-- setup.py

  • setup.py
from setuptools import find_packages

from numpy.distutils.core import setup, Extension

ext1 = Extension(name='my_project.modf90',
                 sources=['src/my_project/funcs_m.f90'],
                 f2py_options=['--quiet'],
                )

ext2 = Extension(name='my_project.oldf90',
                 sources=['src/my_project/two/plus2.f90', 'src/my_project/two/times2.f90'],
                 f2py_options=['--quiet'],
                )

setup(name="my_project",
      version="0.0.1",
      package_dir={"": "src"},
      packages=find_packages(where="src"),
      ext_modules=[ext1, ext2])
  • __init__.py

__init__.py 文件为空。 (例如,如果需要,可以在此处导入 F2PY 模块)

  • mod1.py
def add(a, b):
  """ add inputs a and b, and return """
  return a + b
  • funcs_m.f90
module funcs_m
  implicit none
  contains
    subroutine add(a, b, c)
      integer, intent(in)  :: a
      integer, intent(in)  :: b
      integer, intent(out) :: c
      c = a + b
    end subroutine add
end module funcs_m
  • plus2.f90
subroutine plus2(x, y)
  integer, intent(in)   :: x
  integer, intent(out)  :: y
  y = x + 2
end subroutine plus2
  • times2.f90
subroutine times2(x, y)
  integer, intent(in)   :: x
  integer, intent(out)  :: y
  y = x * 2
end subroutine times2
  • test_my_project.py
import my_project.mod1
import my_project.oldf90
import my_project.modf90

print("mod1.add:            1 + 2 = ", my_project.mod1.add(1, 2))
print("modf90.funcs_m.add:  1 + 2 = ", my_project.modf90.funcs_m.add(1, 2))
x = 1
x = my_project.oldf90.plus2(x)
print("oldf90.plus2:        1 + 2 = ", x)
x = my_project.oldf90.times2(x)
print("oldf90.plus2:        3 * 2 = ", x)

正在安装

现在,可以使用 pip 安装软件包。与 setup.py install 相比,使用 pip(包括易于升级或卸载)有几个 advantages(但这仍然可以用于构建分发包!)。来自包含 setup.py 的目录:

> python -m pip install .

测试

然后,测试刚刚安装的包

> python test_my_project.py
mod1.add:            1 + 2 =  3
modf90.funcs_m.add:  1 + 2 =  3
oldf90.plus2:        1 + 2 =  3
oldf90.plus2:        3 * 2 =  6

此设置已在 Windows 10(使用 ifort)、Ubuntu 18.04(使用 gfortran)和 MacOS High Sierra(使用 gfortran)上成功测试,全部使用 Python 3.6.3.