如何从 Python 包中读取(静态)文件?
Posted
技术标签:
【中文标题】如何从 Python 包中读取(静态)文件?【英文标题】:How to read a (static) file from inside a Python package? 【发布时间】:2011-08-27 01:06:48 【问题描述】:您能告诉我如何读取 Python 包中的文件吗?
我的情况
我加载的包有许多我想从程序中加载的模板(用作字符串的文本文件)。但是如何指定此类文件的路径?
假设我想从以下位置读取文件:
package\templates\temp_file
某种路径操纵?包基本路径跟踪?
【问题讨论】:
相关:MANIFEST.in ignored on “python setup.py install” - no data files installed? 【参考方案1】:包装前奏:
在您甚至可以担心读取资源文件之前,第一步是首先确保将数据文件打包到您的发行版中 - 直接从源代码树中读取它们很容易,但重要的是部分是确保可以从已安装包中的代码访问这些资源文件。
像这样构建您的项目,将数据文件放入包中的子目录:
.
├── 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 或 wheel 中的代码,则此方法不起作用,并且无论您的包是否被提取到文件系统,用户都可能完全无法控制。
坏方法 #2:使用 pkg_resources API
这在投票最多的答案中有所描述。它看起来像这样:
from pkg_resources import resource_string
data = resource_string(__name__, "templates/temp_file")
这有什么问题?它添加了对setuptools 的runtime 依赖,它最好只是install 时间依赖。导入和使用pkg_resources
可能会变得非常缓慢,因为代码会构建所有 已安装包的工作集,即使您只对您自己的 包资源感兴趣。这在安装时没什么大不了的(因为安装是一次性的),但在运行时就很难看。
坏方法 #3:使用旧的 importlib.resources API
这是目前投票率最高的答案中的建议。从 Python 3.7 开始,它就在 standard library 中。它看起来像这样:
from importlib.resources import read_binary
data = read_binary("package.templates", "temp_file")
这有什么问题?好吧,不幸的是,该实现留下了一些不足之处,并且很可能在 Python 3.11 中被弃用。使用importlib.resources.read_binary
、importlib.resources.read_text
和朋友将要求您添加一个空文件templates/__init__.py
,以便数据文件驻留在子包中而不是子目录中。它还将package/templates
子目录作为可导入的package.templates
子包公开。这不适用于许多已经使用资源子目录而不是资源子包发布的现有包,并且到处添加__init__.py
文件会混淆数据和代码之间的界限,这很不方便。
这种方法已经是deprecated in upstream importlib_resources
,预计弃用将出现在版本 3.11 的 CPython 标准库中。 bpo-45514 跟踪弃用,migrating from legacy 提供 _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,它适用于 zips,并且不需要在资源子目录中添加虚假的 __init__.py
文件。与pkgutil
相比,我能看到的唯一缺点是这些新 API 仅在 Python-3.9+ 的 stdlib 中可用,因此仍然需要第三方依赖项来支持较旧的 Python 版本。如果您只需要在 Python-3.9+ 上运行,请使用此方法,或者您可以在较旧的 Python 版本的 backport 上添加兼容层和 conditional dependency:
# 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 了解更多信息。
【讨论】:
去年五月编辑过。但我想很容易错过介绍中的解释。尽管如此,你还是建议人们反对这个标准 - 这是一颗难以忍受的子弹:-) @ankostis 让我把问题转到你身上,尽管有所有这些缺点以及已经是 pending deprecation 的不完整 API,你为什么要推荐importlib.resources
?更新不一定更好。告诉我与 stdlib pkgutil 相比,它实际上提供了哪些优势,您的回答没有提及?
亲爱的@wim,Brett Canon's last response 对pkgutil.get_data()
的使用证实了我的直觉 - 这是一个未开发的、即将被弃用的 API。也就是说,我同意你的观点,importlib.resources
并不是一个更好的选择,但在 PY3.10 解决这个问题之前,我支持这个选择,因为了解到这不仅仅是文档推荐的另一个“标准”。跨度>
@ankostis 我会对 Brett 的 cmets 持保留态度。 pkgutil
根本没有在 PEP 594 -- Removing dead batteries from the standard library 的弃用计划中提及,并且不太可能在没有充分理由的情况下被删除。它自 Python 2.3 以来一直存在,并在 PEP 302 中指定为加载器协议的一部分。使用“未定义的 API”并不是一个很有说服力的回答,它可以描述 Python 标准库的大部分内容!
让我补充一下:我也希望看到 importlib 资源成功! 我完全支持严格定义的 API。只是在目前的状态下,不能真正推荐它。该 API 仍在发生变化,它不能用于许多现有的包,并且仅在相对较新的 Python 版本中可用。在实践中,它几乎在各个方面都比pkgutil
差。您的“直觉”和appeal to authority 对我来说毫无意义,如果get_data
加载程序有问题,请出示证据和实际示例。【参考方案2】:
TLDR; 使用标准库的 importlib.resources
module,如下面方法 2 中所述。
传统的 pkg_resources
from setuptools
不再推荐,因为新方法:
setuptools
),而只依赖 Python 的标准库。
我把传统的先列出来,说明移植现有代码时与新方法的区别(也移植explained here)。
假设您的模板位于嵌套在模块包内的文件夹中:
<your-package>
+--<module-asking-the-file>
+--templates/
+--temp_file <-- We want this file.
注意 1:当然,我们不应该摆弄
__file__
属性(例如,从 zip 提供代码时会中断)。注意 2:如果您正在构建此包,请记住在您的
setup.py
中将您的数据文件声明为package_data
ordata_files
。
1) 使用来自setuptools
的pkg_resources
(慢)
您可以使用 setuptools 发行版中的 pkg_resources
软件包,但 这是有代价的,performance-wise:
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
,和/或使用期待已久的zipapp
packer 从 python-3.5 到创建独立的发行版。记得将
setuptools
添加到您的运行时要求中(例如在 install_requires 中)。
...请注意,根据 Setuptools/pkg_resources
文档,您不应使用 os.path.join
:
Basic Resource Access请注意,资源名称必须是
/
分隔的路径,并且不能是绝对的(即没有前导/
)或包含诸如“..
”之类的相对名称。不要不使用os.path
例程来操作资源路径,因为它们不是文件系统路径。
2) Python >= 3.7,或使用向后移植的 importlib_resources
库
使用标准库的importlib.resources
module,比上面的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
不再是路径,而只是现有包中要打开的资源的文件名;它可能不包含路径分隔符,也可能没有子资源(即它不能是目录)。
对于问题中提出的示例,我们现在必须:
通过在其中创建一个空的__init__.py
文件,将<your_package>/templates/
放入适当的包中,
所以现在我们可以使用一个简单的(可能是相对的)import
语句(不再解析包/模块名称),
只需询问resource_name = "temp_file"
(无路径)。
提示:
要访问当前模块内的文件,请将包参数设置为__package__
,例如pkg_resources.read_text(__package__, 'temp_file')
(感谢@ben-mares)。 当使用path()
询问实际文件名 时事情会变得有趣,因为现在上下文管理器用于临时创建的文件(阅读this)。 使用install_requires=[" importlib_resources ; python_version<'3.7'"]
添加向后移植的库,有条件地适用于较旧的Python(如果您使用setuptools<36.2.1
打包项目,请检查this)。 如果您从传统方法迁移,请记住从 runtime-requirements 中删除setuptools
库。 记得自定义setup.py
或MANIFEST
为include any static files。 您也可以在setup.py
中设置zip_safe=True
。
【讨论】:
str.join 需要序列 resource_path = '/'.join(('templates', 'temp_file')) 我不断收到NotImplementedError: Can't perform this operation for loaders without 'get_data()'
任何想法?
请注意,importlib.resources
和 pkg_resources
不一定兼容。 importlib.resources
与添加到sys.path
的压缩文件一起使用,setuptools 和pkg_resources
与egg 文件一起使用,这些文件是存储在本身添加到sys.path
的目录中的压缩文件。例如。使用sys.path = [..., '.../foo', '.../bar.zip']
,鸡蛋进入.../foo
,但也可以导入bar.zip
中的包。您不能使用pkg_resources
从bar.zip
中的包中提取数据。我还没有检查 setuptools 是否为 importlib.resources
注册了必要的加载器来处理鸡蛋。
如果出现错误Package has no location
,是否需要额外的setup.py配置?
如果您想访问当前模块中的文件(而不是像示例中的templates
这样的子模块),那么您可以将package
参数设置为__package__
,例如pkg_resources.read_text(__package__, 'temp_file')
【参考方案3】:
接受的答案应该是使用importlib.resources
。 pkgutil.get_data
还要求参数 package
是一个非命名空间包 (see pkgutil docs)。因此,包含资源的目录必须有一个__init__.py
文件,使其具有与importlib.resources
完全相同的限制。如果pkg_resources
的开销问题不是问题,这也是一个可以接受的替代方案。
Pre-Python-3.3
,所有包都必须有一个__init__.py
。 Post-Python-3.3
,文件夹不需要 __init__.py
成为一个包。这称为namespace package
。不幸的是,pkgutil
不适用于 namespace packages
(see pkgutil docs)。
以包结构为例:
+-- 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!'
【讨论】:
这个答案是错误的——包含资源的目录不需要是一个包。它可以是包内的子目录。importlib.resources
的限制是 pkgutil
没有的,即包含资源的目录本身也需要有一个__init__.py
,即它必须是一个子包。这与命名空间包问题无关,它关注的是***目录中是否存在__init__.py
,而不是包内的数据子目录中。
@wim 对不起,但我相信你弄错了。 pre-Python 3.3+
,所有包都必须有一个 __init__.py
才能加载。在 3.3 之后,包不需要它们。没有__init__.py
的包是namespace packages
。根据pkgutil
文档,如果您尝试从命名空间包中加载资源,您将获得None
。请查看我更新后的编辑答案。
您错误地使用了pkgutil
。试试pkgutil.get_data("foo", "bar/hi.txt")
【参考方案4】:
如果你有这个结构
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
APIs
另外请注意,如果您使用路径,则必须使用正斜杠 (/) 作为路径分隔符,即使您在 Windows 上也是如此。 Setuptools 在构建时自动将斜杠转换为适当的特定于平台的分隔符
如果您想知道文档在哪里:
PEP 0365 https://packaging.python.org/guides/single-sourcing-package-version/【讨论】:
pkg_resources
具有 pkgutil
克服的开销。此外,如果提供的代码作为入口点运行,__name__
将评估为__main__
,而不是包名称。【参考方案5】:
David Beazley 和 Brian K. Jones 的 Python Cookbook 第三版“10.8.Reading Datafiles Within a Package”中的内容给出了答案。
我就到这里来:
假设你有一个包,其中的文件组织如下:
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。
【讨论】:
我喜欢你引用了食谱!【参考方案6】:包中的每个 python 模块都有一个__file__
属性
您可以将其用作:
import os
from mypackage
templates_dir = os.path.join(os.path.dirname(mypackage.__file__), 'templates')
template_file = os.path.join(templates_dir, 'template.txt')
鸡蛋资源见:http://peak.telecommunity.com/DevCenter/PythonEggs#accessing-package-resources
【讨论】:
这不适用于 zip 文件中包含的源代码。 这不是一个可靠的方法。它会引入脆弱的代码。【参考方案7】:假设您使用的是 egg 文件;未提取:
我在最近的一个项目中“解决”了这个问题,通过使用安装后脚本,该脚本将我的模板从 egg(zip 文件)提取到文件系统中的正确目录。这是我找到的最快、最可靠的解决方案,因为使用__path__[0]
有时会出错(我不记得名字了,但我至少看到了一个库,它在该列表前面添加了一些东西!)。
此外,egg 文件通常会在运行中被提取到一个名为“egg cache”的临时位置。您可以在启动脚本之前甚至之后使用环境变量更改该位置,例如。
os.environ['PYTHON_EGG_CACHE'] = path
但是有 pkg_resources 可能会正确完成这项工作。
【讨论】:
以上是关于如何从 Python 包中读取(静态)文件?的主要内容,如果未能解决你的问题,请参考以下文章