【问题标题】:Link f2py generated *.so file in a python package using setuptools使用 setuptools 在 python 包中链接 f2py 生成的 *.so 文件
【发布时间】:2020-11-22 03:43:52
【问题描述】:

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

我的项目

  • license.txt
  • README.md
  • setup.py
  • 我的项目
    • 初始化.py
    • myfunc.py
    • 你好.so

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

然后我在我的 Ubuntu 上尝试了标准的 setuptools 安装:sudo python3 setup.py install,它安装得很好。但不幸的是,在导入时,它会抛出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 以某种方式实现了这一点,但即使在查看了他们的代码之后,我也无法解码他们是如何做到的。有人可以帮我弄这个吗?提前致谢。

【问题讨论】:

  • 您是否对它只在 Linux 上运行感到高兴,还是它也必须在 Windows 和 Mac 上运行?它只能在(最常见的)x86_64 CPU 上工作还是必须在 i686 上工作?
  • @FiddleStix 如果它适用于 Windows 和 Mac,那就太好了,但我不坚持。另外,x86_64 就足够了。
  • 为了确定,您想预编译 f2py 模块,这样用户自己就不需要 Fortran 工具链(但暗示所有与 Fortran 相关的依赖项都需要静态编译到 .so 中, (.dll 或 .dylib)由您提前为您打算支持的所有平台提供)?或者是否可以选择将 f2py 编译作为安装过程的一部分?
  • @jbdv 对我来说都可以。我很乐意编译和发布,但是那些 .so 应该会自动安装在用户机器上的适当位置。否则,用户编译、生成 .so 并将它们放置在可以由我的包导入的适当位置也很好。
  • 我使用基于您评论的SO answer 的设置。这对你不起作用吗?对于本地安装 (sudo python -m pip install .),它将 .so 文件放置在 .../site-packages/my_project_f90.cpython-36m-x86_64-linux-gnu.so 与其他项目文件相同的级别,例如.../site-packages/my_project-0.1-py3.6.egg-info。在创建车轮分布时会复制相同的结构,例如sudo python setup.py bdist_wheelbuild/bdist.linux-x86_64/wheel。这对你来说是失败的吗?

标签: python-3.x fortran shared-libraries setuptools f2py


【解决方案1】:

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

具有多个 F2PY 扩展模块的最小示例的结构基于 src directory layout。它不是必需的/必需的,但它的优点是除非成功安装包,否则测试例程无法运行。

源布局

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 来安装软件包。有几个advantages 可以使用pip(包括易于升级或卸载)而不是setup.py install(但这仍然可以用于构建分发包!)。从包含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 成功测试。

【讨论】:

  • 这看起来很棒。我会测试它。如果我有多个 fortran 文件,我将不得不为每个文件分别使用 Extension。是对的吗?此外,我的每个 fortran 文件都包含一个单独的 fortran 模块,因此理想情况下它应该为每个单独生成 .so 文件。
  • 与 f2py 文档一样,您必须为每个要创建的单独 f2py 模块/.so 创建一个 ext_i = Extension(...),然后将它们添加到 setup.py 中的 ext_modules=[ext_1,...,ext_i]。如果单个 f2py 模块包含多个 Fortran 源文件,则将它们列在特定的 ext_isources 参数下。有时间我会更新我的答案
  • 这适用于 Linux!我将在 Mac 和 Windows 上进行测试,然后接受答案。
  • 不客气,我本人对这个设置非常满意,这要归功于数小时的试验/挫折,以及最终的出色文档:-)
  • 在 Windows 上,用户是否需要安装 fortran 才能进行此安装?
【解决方案2】:

您可以使用像这样的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 只是一个扩展实例列表,每个实例描述一组扩展模块。在上面的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

【讨论】:

  • 谢谢。我正在测试这个。我的包名包含一个连字符:foo-bar,而实际目录名是foo_bar(带下划线)。在 setuptools.find_packages 中我使用foo-bar 还是foo_bar
  • @Peaceful 不客气。在这种情况下,您需要将foo-bar 重命名为foo_barfoo_bar 有效),否则虽然您可以构建包,但由于包名称中的-,您无法导入它。
  • 这对我有用!我唯一需要更改的是将packages=setuptools.find_packages("foo") 更改为包含模块名称和子模块名称['foo', 'foo/bar'] 的列表。但在我接受这个答案之前,正如我所说,我也想了解正在发生的事情。然后我会奖励赏金!
  • @Peaceful 干杯!查看我更新的帖子,我提供了一个紧凑的setup.py 文件。
  • 这在 Windows 上也能工作吗?这甚至可以在 Windows 上使用吗?我所做的另一个更改是使用 numpy.f2py 代替 f2py。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-08-20
  • 2014-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-29
  • 1970-01-01
相关资源
最近更新 更多