包含数据文件
在本节中,您将
- 了解将大型文件保留在软件包之外的重要性。
- 学习一些替代方法。
- 学习如何在软件包中包含小型数据文件。
考虑替代方案
切勿在您的 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 检索其数据。有一些免费的数据托管选项,例如 Zenodo、osf.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",
)
在重复运行此命令时,将使用本地缓存的文件名,而不是再次下载数据。