目录

包含数据文件

在本节中,您将

  • 了解将大型文件保留在软件包之外的重要性。
  • 学习一些替代方法。
  • 学习如何在软件包中包含小型数据文件。

考虑替代方案

切勿在您的 Python 软件包或 Git 仓库中包含大型二进制文件。 一旦提交,文件将永远保存在 Git 历史记录中。Git 会变得迟缓,因为它不是为处理大型二进制文件而设计的,并且您的软件包将成为一个令人讨厌的大型下载。

事后删除意外提交的文件是可能的,但具有破坏性,因此务必避免首先提交大型文件。

替代方案

  • 您可以使用代码生成文件吗?这对于测试数据来说是一种不错的方法:在测试过程中生成测试数据文件。当然,定期测试真实数据非常重要,但对于自动化测试,模拟数据就足够了。如果您对数据了解不够,无法准确地模拟它,那么您对它的了解就不足以编写针对它的有用测试。
  • 您可以编写一个 Python 函数,根据需要从某个公共 URL 获取数据吗?这是 scikit-learn 等项目使用的方法,这些项目需要下载大型数据集以供其示例和测试使用。

如果您使用其中一种替代方案,请将生成或下载的文件名添加到项目的 .gitignore 文件中,该文件由 copier/cookiecutter 模板提供。这有助于防止您意外地将文件提交到 Git。

如果相关文件是文本文件并且不大(< 100 kB),那么将其与软件包捆绑在一起是合理的。否则,请参阅最后的建议。

如何打包数据文件

我们在这里解决什么问题?如果您的 Python 程序需要访问数据文件,那么最简单的解决方案就是硬编码该文件的路径。

from pathlib import Path

spacings_txt = Path("peak_spacings/LaB6.txt").read_text(encoding="utf-8")

但这并不是一个好的解决方案,因为

  • 数据文件不会包含在分发版中:使用 pip install 安装您的软件包的用户会发现它丢失了!
  • 数据文件的路径取决于平台以及软件包的安装方式。我们需要 Python 为我们处理这些细节。
  • 您的软件包甚至可能没有安装在文件系统上,它可能位于 zip 文件或数据库中。

例如,假设我们有一些包含各种晶体结构布拉格峰间距的文本文件,并且我们想在我们的 Python 软件包中使用这些文件。让我们将它们放在软件包中的一个新目录中,例如 src/package/peak_spacings/

# src/package/peak_spacings/LaB6.txt

4.15772
2.94676
2.40116
# src/package/peak_spacings/Si.txt

3.13556044
1.92013079
1.63749304
1.04518681

为了使这些文件可供 Python 加载机制使用,最简单的方法是在 src/package/peak_spacing 中添加一个 __init__.py。这可以是一个空文件;其目的是告诉 Python 可以加载它。

您需要确保您的 Python 构建后端将这些文件放置在 SDist 和 wheel 中。如果您使用的是 setuptools 之外的任何其他工具,则这应该是自动的。

Setuptools 特定说明

有两种方法可以在 setuptools 中包含数据文件。您可以显式列出软件包数据

# setup.cfg
[options.package_data]
package.peak_spacings =
    *.txt

或者,您可以使用自动数据包含(如果您在 Setuptools 61+ 中使用 pyproject.toml [project] 配置,则这是默认设置)。要使用 setup.cfg 启用此功能

[options]
include_package_data = True

但是,您还需要确保文件也位于 SDist 中

# MANIFEST.in
include src/package/peak_spacings/*.txt

最后,无论我们在科学代码中实际使用文件的哪个位置,我们都可以使用 importlib_resources 访问它们。对于 Python >= 3.9 的用户,可以直接使用标准库 importlib.resources 模块,而无需依赖 importlib_resources

# from importlib import resources   # Python >= 3.9 only
import importlib_resources as resources


ref = resources.files("package.peak_spacings") / "LaB6.txt"

spacings_txt = ref.read_text(encoding="utf-8")

# If you have an API that requires an on-disk file, you can do this instead:
with resources.as_file(ref) as path:
    # Now path is guaranteed to live somewhere on disk
    with path.open(encoding="utf-8") as f:
        spacings_txt = f.read()

使用 __init__

与其使用空的 init,不如将 files(...) 移动到 __init__.py 中。这将如下所示

import importlib_resources as resources

files = resources.files(__name__)
LaB6 = files / "LaB6.txt"
# Provide whatever is useful for your project here

现在,用户只需导入并直接使用 package.peakspacing.LaB6 等。

按需下载大型文件

一个常见的用例是,项目可能有一些示例笔记本或演示脚本,这些脚本需要项目本身未分发的数据。在这种情况下,一种方法是提供一个下载脚本,用户可以使用该脚本从提供的 URL 检索其数据。有一些免费的数据托管选项,例如 Zenodoosf.io 或 GitHub 或 GitLab 上的服务上的特定于数据存储库。

一些项目有多个用于示例或测试的大型数据集,可以以对软件包用户透明的方式在首次使用时自动下载到本地缓存。例如,scipy.datasets 下的数据集并不存在于主 SciPy 存储库中,而是存储在 SciPy GitHub 组织下的独立存储库中。一个名为 Pooch 的工具用于在用户首次请求这些数据集时从外部存储库 URL 为用户获取这些数据集。Pooch 负责比较下载数据的哈希值以验证其内容,然后缓存下载的文件以供将来重用。

SciPy 的 scipy.datasets 模块和 scikit-image 的 skimage.data 模块是项目中如何以这种方式使用 Pooch 的两个具体示例。也可以在更简单的下载脚本中使用 Pooch 来仅获取单个文件,如下面的示例所示

import pooch

file_path = pooch.retrieve(
    # URL to my data
    url="https://github.com/org/project/raw/v1.0.0/data/test_image.jpg",
    known_hash="sha256:50ef9a52c621b7c0c506ad1fe1b8ee8a158a4d7c8e50ddfce1e273a422dca3f9",
)

在重复运行此命令时,将使用本地缓存的文件名,而不是再次下载数据。