如何从 Python 包内部读取(静态)文件?
- 2024-12-11 08:47:00
- admin 原创
- 166
问题描述:
你能告诉我如何读取我的 Python 包里的文件吗?
我的情况
我加载的包中有许多模板(用作字符串的文本文件),我想从程序中加载这些模板。但是我如何指定此类文件的路径?
假设我想从以下位置读取一个文件:
package emplates emp_file
某种路径操作?软件包基本路径跟踪?
解决方案 1:
TLDR;使用标准库的importlib.resources
模块
如果你不关心向后兼容性<Python 3.9(在下面的方法2中详细说明)请使用以下命令:
from importlib import resources as impresources
from . import templates
inp_file = impresources.files(templates) / 'temp_file'
with inp_file.open("rt") as f:
template = f.read()
细节
不再推荐使用传统方法,因为新方法 :pkg_resources
`setuptools`
其性能显著提高;
更安全,因为使用包(而不是路径字符串)会引发编译时错误;
它更直观,因为您不必“加入”路径;
仅依赖于 Python 的标准库(没有额外的 3rdp 依赖
setuptools
)。
我首先列出传统的方法,以解释移植现有代码时与新方法的区别(移植也在此处解释)。
假设您的模板位于模块包内嵌套的文件夹中:
<your-package>
+--<module-asking-the-file>
+--templates/
+--temp_file <-- We want this file.
注 1:当然,我们不应该摆弄
__file__
属性(例如,从 zip 提供时代码将会中断)。注 2:如果您正在构建此包,请记住
package_data
在data_files
您的setup.py
.
1)使用pkg_resources
from setuptools
(慢)
您可以使用pkg_resources
来自setuptools分发包的包,但从性能方面来说这需要付出一些代价:
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)
尖端:
即使您的分发版已压缩,这也将读取数据,因此您可以
zip_safe=True
在您的版本中进行设置setup.py
,和/或使用期待已久的python -3.5zipapp
打包程序来创建自包含的分发版。记得添加
setuptools
到你的运行时要求中(例如在 install_requires` 中)。
...请注意,根据 Setuptools/pkg_resources
文档,您不应使用os.path.join
:
基本资源访问
请注意,资源名称必须是以
/
分隔的路径,不能是绝对路径(即没有前导/
)或包含相对名称(如“..
”)。不要使用例程os.path
来操作资源路径,因为它们不是文件系统路径。
2)Python >= 3.7,或使用反向移植的importlib_resources
库
使用比上述更高效的标准库importlib.resources
模块:setuptools
try:
from importlib import resources as impresources
except ImportError:
# Try backported to PY<37 `importlib_resources`.
import importlib_resources as impresources
from . import templates # relative-import the *package* containing the templates
try:
inp_file = (impresources.files(templates) / 'temp_file')
with inp_file.open("rb") as f: # or "rt" as text file with universal newlines
template = f.read()
except AttributeError:
# Python < PY3.9, fall back to method deprecated in PY3.11.
template = impresources.read_text(templates, 'temp_file')
# or for a file-like stream:
template = impresources.open_text(templates, 'temp_file')
注意力:
关于功能
read_text(package, resource)
:
可以
package
是字符串,也可以是模块。不再是路径
resource
,而仅仅是现有包中要打开的资源的文件名;它可能不包含路径分隔符,也可能没有子资源(即它不能是目录)。
对于问题中提出的例子,我们现在必须:
通过在其中创建一个空文件,将其变成
<your_package>/templates/
合适的包,__init__.py
所以现在我们可以使用一个简单的(可能是相对的)
import
语句(不再解析包/模块名称),并简单地要求
resource_name = "temp_file"
(无路径)。
尖端:
要访问当前模块内的文件,请将包参数设置为
__package__
,例如impresources.read_text(__package__, 'temp_file')
(感谢@ben-mares)。当询问实际的文件名时,事情变得有趣
path()
,因为现在上下文管理器用于临时创建的文件(阅读此内容)。有条件地为较旧的 Python 添加反向移植的库
install_requires=[" importlib_resources ; python_version<'3.7'"]
(如果您使用打包项目,请选中此项setuptools<36.2.1
)。如果您从传统方法迁移,请记得从运行时要求中删除
setuptools
库。请记住自定义
setup.py
或MANIFEST
包含任何静态文件。您也可以
zip_safe=True
在您的 中设置setup.py
。
解决方案 2:
包装前奏:
在您担心读取资源文件之前,第一步是确保数据文件首先被打包到您的发行版中 - 直接从源树中读取它们很容易,但重要的是确保这些资源文件可以从已安装包中的代码访问。
像这样构建您的项目,将数据文件放入包内的子目录中:
. <--- project root
├── package <--- source root
│ ├── __init__.py
│ ├── templates <--- resources subdirectory
│ │ └── temp_file <--- this is a data file, not code
│ ├── mymodule1.py
│ └── mymodule2.py
├── README.rst
├── MANIFEST.in
└── setup.py
您应该传入include_package_data=True
调用setup()
。仅当您想使用 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")
它在 zip 中运行。它适用于 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 的运行时依赖,而 setuptools 最好只是安装时依赖。导入和使用可能会变得非常慢,因为代码会构建所有已安装软件包的工作集,即使您只对自己的软件包资源感兴趣。这在安装时不是什么大问题(因为安装是一次性的),但在运行时就很糟糕了。pkg_resources
错误方法 #3:使用旧版 importlib.resources API
这是目前之前得票最高的答案的推荐。它自 Python 3.7 以来就包含在标准库中。它看起来像这样:
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`
这种方法在 2021 年的上游已被弃用importlib_resources
,并且从 Python 3.11 版本开始在 stdlib 中已被弃用。bpo-45514跟踪了弃用情况并从旧版优惠包装器迁移_legacy.py
以帮助过渡。
更令人困惑的是,函数式 API 可能在 Python 3.13 中再次变为“不再被推荐”,并且名称保持不变,但用法略有不同:read_binary
,read_text
。
荣誉提名:使用可遍历的 importlib 资源 API
当我发布此内容时(2020),得票最高的答案中尚未提及这一点,但作者随后将其编辑到他们的答案中(2023)。importlib_resources
不仅仅是 Python 3.7+importlib.resources
代码的简单反向移植。它具有可遍历的 API,用于访问资源,其用法类似于pathlib
:
import importlib_resources
my_resources = importlib_resources.files("package")
data = my_resources.joinpath("templates", "temp_file").read_bytes()
这适用于 Python 2 和 3,可以在 zip 中使用,并且不需要__init__.py
在资源子目录中添加虚假文件。pkgutil
我能看到的唯一缺点是可遍历 API 仅在importlib.resources
Python-3.9+ 的 stdlib 中可用,因此仍然需要第三方依赖项来支持较旧的 Python 版本。如果您只需要在 Python-3.9+ 上运行,请使用此方法,或者您可以为较旧的 Python 版本添加兼容层和对反向移植的条件依赖:
# 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"',
]
)
在 Python 3.8 终止使用(2024 年 10 月)之前,我仍然建议使用 stdlib pkgutil
,以避免条件依赖带来的额外复杂性。
示例项目:
我在GitHub上创建了一个示例项目并上传到PyPI,该项目演示了上面讨论的所有五种方法。尝试一下:
$ pip install resources-example
$ resources-example
有关更多信息,请参阅https://github.com/wimglenn/resources-example。
解决方案 3:
David Beazley 和 Brian K. Jones 所著的《Python Cookbook》第三版中“10.8. 读取包内的数据文件”的内容给出了答案。
我就把它放在这里吧:
假设您有一个包,其中的文件组织如下:
mypackage/
__init__.py
somedata.dat
spam.py
现在假设文件 spam.py 想要读取文件 somedata.dat 的内容。为此,请使用以下代码:
import pkgutil
data = pkgutil.get_data(__package__, 'somedata.dat')
生成的变量数据将是一个包含文件原始内容的字节字符串。
get_data() 的第一个参数是包含包名称的字符串。您可以直接提供它,也可以使用特殊变量,例如__package__
。第二个参数是包内文件的相对名称。如有必要,您可以使用标准 Unix 文件名约定导航到不同的目录,只要最终目录仍位于包内即可。
这样,软件包就可以作为目录、.zip 或 .egg 安装。
解决方案 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
API
还要注意,如果你使用路径,你必须使用正斜杠 (/) 作为路径分隔符,即使你在 Windows 上。Setuptools 会在构建时自动将斜杠转换为适当的平台特定分隔符
如果你想知道文档在哪里:
解决方案 5:
这是我的标准做法
import importlib.resources as resources
from <your_package> import __name__ as pkg_name
template_path = resources.files(pkg_name) / "template" / "temp_file"
with template_path.open() as f:
template = f.read()
顺便提一下,受到Maven 标准目录布局的启发,我建议采用以下项目结构,其中resources
包含包目录和测试目录内的文件夹:
.
├── pyproject.toml
├── src
│ └── <your_package>
│ └── resources
└── tests
└── resources
然后你temp_file
就可以进入resources
文件夹并访问文件
template_path = resources.files(pkg_name) / "resources" / "temp_file"
解决方案 6:
可接受的答案应该是使用importlib.resources
。pkgutil.get_data
还要求参数package
是非命名空间包(请参阅 pkgutil 文档)。因此,包含资源的目录必须有一个__init__.py
文件,使其具有与完全相同的限制importlib.resources
。如果不担心的开销问题pkg_resources
,这也是一个可接受的替代方案。
Pre-Python-3.3
,所有软件包都需要有一个__init__.py
。Post-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
然而,__init__.py
有了bar
>>> import pkgutil
>>> rsrc = pkgutil.get_data("foo.bar", "hi.txt")
>>> print(rsrc)
b'Hi!'
解决方案 7:
假设你正在使用 egg 文件;未提取:
我在最近的一个项目中“解决”了这个问题,方法是使用一个安装后脚本,将我的模板从 egg(zip 文件)提取到文件系统中的正确目录中。这是我找到的最快、最可靠的解决方案,因为使用__path__[0]
有时会出错(我不记得名字了,但我至少遇到了一个库,它在列表前面添加了一些东西!)。
此外,egg 文件通常会被即时提取到名为“egg 缓存”的临时位置。您可以使用环境变量更改该位置,无论是在启动脚本之前还是之后,例如:
os.environ['PYTHON_EGG_CACHE'] = path
然而,pkg_resources或许能够正确完成这项工作。