在Python项目中管理资源

71

我有一个Python项目,其中使用了许多非代码文件。目前这些文件都是图片,但未来可能会使用其他类型的文件。如何存储和引用这些文件才是最佳方案?

我考虑只在主目录中创建一个名为“resources”的文件夹,但存在一个问题:我的一些子包中使用了这些图像。以这种方式存储这些图像将导致耦合,这是一个劣势。

另外,我需要一种独立于当前目录的访问这些文件的方法。

4个回答

68
你可能想要使用与 setuptools 一起提供的 pkg_resources 库。
例如,我编写了一个快速的小包 "proj" 来说明我将使用的资源组织方案:
proj/setup.py
proj/proj/__init__.py
proj/proj/code.py
proj/proj/resources/__init__.py
proj/proj/resources/images/__init__.py
proj/proj/resources/images/pic1.png
proj/proj/resources/images/pic2.png
请注意,我将所有资源保留在单独的子包中。 "code.py" 显示了如何使用 pkg_resources 引用资源对象:
from pkg_resources import resource_string, resource_listdir

# Itemize data files under proj/resources/images:
print resource_listdir('proj.resources.images', '')
# Get the data file bytes:
print resource_string('proj.resources.images', 'pic2.png').encode('base64')
如果运行它,您将得到:
['__init__.py', '__init__.pyc', 'pic1.png', 'pic2.png']
iVBORw0KGgoAAAANSUhE ...
如果您需要将资源视为文件对象,则使用resource_stream()
访问资源的代码可能位于项目的子包结构中的任何位置,只需通过完整名称引用包含图像的子包:proj.resources.images,在这种情况下。
这是"setup.py":
#!/usr/bin/env python

from setuptools import setup, find_packages

setup(name='proj',
      packages=find_packages(),
      package_data={'': ['*.png']})

注意:为了在未安装软件包的情况下“本地”测试,您必须从具有setup.py的目录中调用测试脚本。如果您位于与code.py相同的目录中,则Python不会知道proj包。因此,像proj.resources这样的内容将无法解析。


9
这里的缺点太多了。难道没有一种明智简单的方法来将资源与Python项目打包吗? - Ram Rachum
1
我只知道两种被广泛支持的方式(不幸的是它们都不简单):1)distutils方式(标准):文档将访问资源文件留给读者自己练习(可能是因为他们认为相对于__file__的路径操作就足够了)。2)setuptools方式(distutils的超集),如上所述。 - Pavel Repin
1
很惊讶这个问题还没有被提出,但是对于resource_string的输出,应该使用decode而不是encode,不是吗? - archeezee
@archeezee 这篇文章是来自 Python 2.x 时代的。在这种情况下,resource_string 返回 pic2.png 的原始字节表示,如果你想要打印它的话,这并不理想 :) 因此需要进行 base64 编码调用。 - Pavel Repin
这似乎不太令人满意,因为它必然会污染“包空间”。例如,在您的示例中,resources 必须proj 的子目录,否则当有人使用 pip 安装我的项目时,我将用 resources 污染他们的包空间(import resources 将获取我的资源目录)。非常烦人的是,由于 Python 已经将其与完全不相关的源代码包概念耦合在一起,所以我放置数据的位置必须受到限制。 - Jack M

16

现在做这件事的新方法是使用importlib。对于Python版本低于3.7的情况,您可以添加一个依赖项importlib_resources并执行以下操作:

from importlib_resources import files


def get_resource(module: str, name: str) -> str:
    """Load a textual resource file."""
    return files(module).joinpath(name).read_text(encoding="utf-8")
如果您的资源位于foo/resources子模块内,那么您将像这样使用get_resource
resource_text = get_resource('foo.resources', 'myresource')

2
自从3.9版本以来,似乎现在是importlib.resources.files(package)文档)。 - bossi

6
您可以在每个需要它的子包中始终拥有一个单独的“资源”文件夹,并使用os.path函数从您的子包的__file__值中获取这些内容。为了说明我的意思,我在三个位置创建了以下__init__.py文件:

c:\temp\topp        (顶层包)
c:\temp\topp\sub1   (子包1)
c:\temp\topp\sub2   (子包2)

下面是__init__.py文件:

import os.path
resource_path = os.path.join(os.path.split(__file__)[0], "resources")
print resource_path

在c:\temp\work目录中,我创建了一个名为topapp.py的应用程序,内容如下:
import topp
import topp.sub1
import topp.sub2

这表示应用程序使用topp包和子包。然后我运行它:

C:\temp\work>topapp
Traceback (most recent call last):
  File "C:\temp\work\topapp.py", line 1, in 
    import topp
ImportError: No module named topp

这是预期的。我们设置PYTHONPATH以模拟将我们的包加入到路径中:

C:\temp\work>set PYTHONPATH=c:\temp

C:\temp\work>topapp
c:\temp\topp\resources
c:\temp\topp\sub1\resources
c:\temp\topp\sub2\resources

正如您所看到的,资源路径正确地解析为路径上实际(子)包的位置。

更新: 这里是相关的py2exe文档。


但是当您想要将整个东西 py2exe 化时呢? - Ram Rachum
我不是在谈论如何将资源与代码打包在一起。我在谈论的是 __file__ 不起作用的事实。 - Ram Rachum

1

在pycon2009上,有一个关于distutils和setuptools的演示。你可以在这里找到所有的视频。

Python中的Eggs和Buildout部署-第1部分

Python中的Eggs和Buildout部署-第2部分

Python中的Eggs和Buildout部署-第3部分

在这些视频中,他们描述了如何在您的软件包中包含静态资源。我相信这是在第2部分中。

使用setuptools,您可以定义依赖项,这将允许您拥有使用第三方软件包资源的两个软件包。

Setuptools还为您提供了一种标准的访问这些资源的方式,并允许您在软件包内使用相对路径,从而消除了担心软件包安装位置的需要。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接