如何从Python包内读取(静态)文件?

你能告诉我如何读取Python包中的文件吗?

我的情况

我加载的包有许多模板(作为字符串使用的文本文件),我想从程序中加载它们。但是我如何指定这样的文件的路径?

假设我想读取一个文件:

package\templates\temp_file

某种路径操作?包基本路径跟踪?

111409 次浏览

假设您正在使用卵文件;不提取:

我在最近的一个项目中“解决”了这个问题,通过使用postinstall脚本,它将我的模板从egg (zip文件)提取到文件系统中的适当目录中。这是我发现的最快,最可靠的解决方案,因为使用__path__[0]有时会出错(我不记得名字了,但我至少看到一个库,它在列表前面添加了一些东西!)

此外,egg文件通常会被提取到一个称为“egg缓存”的临时位置。您可以使用环境变量更改该位置,可以在启动脚本之前或稍后,例如。

os.environ['PYTHON_EGG_CACHE'] = path

然而,有pkg_resources可以正确地完成这项工作。

TLDR;使用标准库的importlib.resources模块,如下面的方法2所述。

不再推荐使用传统的 __ABC0从setuptools,因为新方法:

  • 它是性能显著提高;
  • Is更安全,因为使用包(而不是路径-sting)会引发编译时错误;
  • 它更直观,因为你不必“加入”;路径;
  • 它在开发时更快,因为你不需要额外的依赖项(setuptools),但只依赖Python的标准库。

我先保留了传统的方法,以解释移植现有代码时与新方法的差异(也移植这里介绍的)。



让我们假设你的模板位于模块包内嵌套的文件夹中:

  <your-package>
+--<module-asking-the-file>
+--templates/
+--temp_file                         <-- We want this file.

注1:当然,我们不应该篡改__file__属性(例如,当从zip中提供代码时,代码将中断)。

如果你正在构建这个包,记得在你的setup.py中将你的数据文件声明为__ABC0或data_files

1)从setuptools使用pkg_resources(缓慢)

你可以使用setuptools发行版中的pkg_resources包,但是这是有代价的,性能方面的:

import pkg_resources


# Could be any dot-separated package/module name or a "Requirement"
resource_package = __name__
resource_path = '/'.join(('templates', 'temp_file'))  # Do not use os.path.join()
template = pkg_resources.resource_string(resource_package, resource_path)
# or for a file-like stream:
template = pkg_resources.resource_stream(resource_package, resource_path)

小贴士:

  • 这将读取数据,即使你的发行版是压缩的,所以你可以在你的setup.py中设置zip_safe=True,和/或使用期待已久的python - 3.5中的zipapp封隔器来创建自包含的发行版。

  • 记住将setuptools添加到您的运行时需求中(例如在install_requires '中)。

... 注意,根据Setuptools/pkg_resources文档,你不应该使用os.path.join:

基本资源访问

请注意,资源名必须是/分隔的路径,不能是绝对路径(即没有/前导)或包含相对名称,如".."。做使用os.path例程来操作资源路径,因为它们是文件系统路径。

2) Python >= 3.7,或者使用反向移植的importlib_resources

使用标准库的importlib.resources模块,它比上面的setuptools更有效:

try:
import importlib.resources as pkg_resources
except ImportError:
# Try backported to PY<37 `importlib_resources`.
import importlib_resources as pkg_resources


from . import templates  # relative-import the *package* containing the templates


template = pkg_resources.read_text(templates, 'temp_file')
# or for a file-like stream:
template = pkg_resources.open_text(templates, 'temp_file')

注意:

关于函数read_text(package, resource):

  • package既可以是字符串,也可以是模块。
  • resource不再是路径,而只是现有包中要打开的资源的文件名;它可能不包含路径分隔符,也可能没有子资源(即它不能是目录)。

对于问题中问到的例子,我们现在必须:

  • 通过在<your_package>/templates/中创建一个空的__init__.py文件,使<your_package>/templates/成为一个合适的包,
  • 所以现在我们可以使用一个简单的(可能是相对的)import语句(不再解析包/模块名称),
  • 并简单地请求resource_name = "temp_file"(无路径)。

小贴士:

  • 要访问当前模块内的文件,将package参数设置为__package__,例如pkg_resources.read_text(__package__, 'temp_file')(感谢@ben-mares)。
  • 当使用path()询问实际的文件名时,事情变得有趣起来,因为现在上下文管理器用于临时创建的文件(读取)。
  • 使用install_requires=[" importlib_resources ; python_version<'3.7'"](如果你用setuptools<36.2.1打包你的项目,请检查)添加后移植库,对于旧的python是有条件的。
  • 如果您从传统方法迁移,请记住从您的运行条件中删除setuptools库。
  • 记住自定义setup.pyMANIFEST包括任何静态文件
  • 你也可以在你的setup.py中设置zip_safe=True

如果你有这样的结构

lidtk
├── bin
│   └── lidtk
├── lidtk
│   ├── analysis
│   │   ├── char_distribution.py
│   │   └── create_cm.py
│   ├── classifiers
│   │   ├── char_dist_metric_train_test.py
│   │   ├── char_features.py
│   │   ├── cld2
│   │   │   ├── cld2_preds.txt
│   │   │   └── cld2wili.py
│   │   ├── get_cld2.py
│   │   ├── text_cat
│   │   │   ├── __init__.py
│   │   │   ├── README.md   <---------- say you want to get this
│   │   │   └── textcat_ngram.py
│   │   └── tfidf_features.py
│   ├── data
│   │   ├── __init__.py
│   │   ├── create_ml_dataset.py
│   │   ├── download_documents.py
│   │   ├── language_utils.py
│   │   ├── pickle_to_txt.py
│   │   └── wili.py
│   ├── __init__.py
│   ├── get_predictions.py
│   ├── languages.csv
│   └── utils.py
├── README.md
├── setup.cfg
└── setup.py

你需要这样的代码:

import pkg_resources


# __name__ in case you're within the package
# - otherwise it would be 'lidtk' in this example as it is the package name
path = 'classifiers/text_cat/README.md'  # always use slash
filepath = pkg_resources.resource_filename(__name__, path)

奇怪的“总是使用斜杠”部分来自setuptools api

还要注意,如果使用路径,必须使用正斜杠(/)作为路径分隔符,即使在Windows上也是如此。Setuptools在构建时自动将斜杠转换为适当的平台特定的分隔符

如果你想知道文档在哪里:

10.8.中的内容。在Python Cookbook第三版中,David Beazley和Brian K. Jones给出了答案。

我把它放到这里:

假设你有一个包,里面的文件组织如下:

mypackage/
__init__.py
somedata.dat
spam.py
现在假设spam.py文件想要读取文件somedata.dat的内容。要做 它,使用以下代码:

import pkgutil
data = pkgutil.get_data(__package__, 'somedata.dat')

结果变量数据将是一个字节字符串,包含文件的原始内容。

get_data()的第一个参数是一个包含包名的字符串。你可以 要么直接提供,要么使用一个特殊的变量,比如__package__。第二个 参数是包中文件的相对名称。如果有必要,您可以导航 使用标准的Unix文件名约定进入不同的目录,只要

. Final目录仍然位于包中

这样,包可以安装为目录,.zip或.egg。

包装前奏:

在你甚至可以担心读取资源文件之前,第一步是确保数据文件首先被打包到你的发行版中——直接从源树中读取它们很容易,但重要的部分是确保这些资源文件可以从安装包中的代码中访问。

像这样构造你的项目,将数据文件放入包的子目录:

.
├── package
│   ├── __init__.py
│   ├── templates
│   │   └── temp_file
│   ├── mymodule1.py
│   └── mymodule2.py
├── README.rst
├── MANIFEST.in
└── setup.py

你应该在setup()调用中传递include_package_data=True。只有当您想使用setuptools/distutils和构建源代码发行版时,才需要清单文件。为了确保这个示例项目结构的templates/temp_file被打包,在清单文件中添加如下一行:

recursive-include package *

历史笔记: 现代构建后端不需要使用清单文件如flit、poetry,其中默认将包含包数据文件。所以,如果你正在使用pyproject.toml,而你没有setup.py文件,那么你可以忽略所有关于MANIFEST.in的东西。

现在,包装结束,进入阅读部分……

推荐:

使用标准库pkgutil api。它在库代码中是这样的:

# within package/mymodule1.py, for example
import pkgutil


data = pkgutil.get_data(__name__, "templates/temp_file")

它有拉链。它适用于Python 2和Python 3。它不需要第三方依赖关系。我真的不知道有任何缺点(如果你是,那么请评论答案)。

避免的坏方法:

错误方法1:使用源文件的相对路径

这是目前公认的答案。最好的情况是这样的:

from pathlib import Path


resource_path = Path(__file__).parent / "templates"
data = resource_path.joinpath("temp_file").read_bytes()

这有什么不对吗?假定您有可用的文件和子目录是不正确的。如果执行的代码是压缩在一个zip包或一个轮子中,这种方法是行不通的,而且是否将包提取到文件系统可能完全不在用户的控制范围内。

坏方法2:使用pkg_resources api

这在投票最多的答案中有描述。它看起来是这样的:

from pkg_resources import resource_string


data = resource_string(__name__, "templates/temp_file")

这有什么不对吗?它在setuptools上添加了一个运行时依赖项,最好只是一个安装时间依赖项。导入和使用pkg_resources会变得非常慢,因为代码构建了一个所有安装包的工作集,即使你只对你自己的包资源感兴趣。这在安装时不是什么大问题(因为安装是一次性关闭的),但在运行时就很难看了。

坏方法3:使用遗留的importlib。资源的api

这是目前投票最多的答案中的建议。它自Python 3.7起就在标准库中。它是这样的:

from importlib.resources import read_binary


data = read_binary("package.templates", "temp_file")

这有什么不对吗?不幸的是,这个实现留下了一些不尽如人意的地方,它很可能在Python 3.11中被弃用。使用importlib.resources.read_binaryimportlib.resources.read_text和朋友将要求您添加一个空文件templates/__init__.py,以便数据文件驻留在子包中,而不是在子目录中。它还将公开package/templates子目录作为一个可导入的package.templates子包。这将不适用于许多已经使用资源子目录而不是资源子包发布的现有包,并且在任何地方添加__init__.py文件都不方便,这会混淆数据和代码之间的边界。

这种方法已经是在上游importlib_resources中已弃用,预计弃用将出现在CPython标准库3.11版中。bpo - 45514跟踪弃用,而从遗留迁移提供_legacy.py包装器来帮助转换。

值得一提的是:使用更新的importlib_resources api

这一点在其他答案中还没有提到,但importlib_resources不仅仅是Python 3.7+ importlib.resources代码的简单后移植。它有可遍历的api,你可以像这样使用:

import importlib_resources


my_resources = importlib_resources.files("package")
data = (my_resources / "templates" / "temp_file").read_bytes()

这适用于Python 2和3,它可以在zip中工作,并且它不需要在资源子目录中添加虚假的__init__.py文件。我所能看到的与pkgutil相比的唯一缺点是,这些新api仅在Python-3.9+的标准库中可用,因此仍然需要第三方依赖来支持旧的Python版本。如果你只需要在Python-3.9+上运行,那么使用这种方法,或者你可以为旧版本的Python添加兼容层和backport上的有条件的依赖性:

# in your library code:
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files


# in your setup.py or similar:
from setuptools import setup
setup(
...
install_requires=[
'importlib_resources; python_version < "3.9"',
]
)

示例项目:

我已经在github上创建了一个示例项目,并上传到了PyPI上,它演示了上面讨论的所有五种方法。试试:

$ pip install resources-example
$ resources-example

更多信息请参见https://github.com/wimglenn/resources-example

接受的答案应该是使用importlib.resourcespkgutil.get_data还要求参数package是一个非命名空间包(请参阅pkgutil文档)。因此,包含资源的目录必须有一个__init__.py文件,使其具有与importlib.resources完全相同的限制。如果pkg_resources的开销问题不是问题,这也是一个可接受的替代方案。

Pre-Python-3.3时,所有包都必须有__init__.pyPost-Python-3.3,一个文件夹不需要__init__.py作为一个包。这被称为namespace package。不幸的是,pkgutil不能与namespace packages (请参阅pkgutil文档)一起工作。

例如,对于包结构:

+-- foo/
|   +-- __init__.py
|   +-- bar/
|   |   +-- hi.txt

其中hi.txt只有Hi!,你得到以下

>>> import pkgutil
>>> rsrc = pkgutil.get_data("foo.bar", "hi.txt")
>>> print(rsrc)
None

然而,在bar中使用__init__.py,你会得到

>>> import pkgutil
>>> rsrc = pkgutil.get_data("foo.bar", "hi.txt")
>>> print(rsrc)
b'Hi!'