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

2024-12-11 08:47:00
admin
原创
166
摘要:问题描述:你能告诉我如何读取我的 Python 包里的文件吗?我的情况我加载的包中有许多模板(用作字符串的文本文件),我想从程序中加载这些模板。但是我如何指定此类文件的路径?假设我想从以下位置读取一个文件:package emplates emp_file 某种路径操作?软件包基本路径跟踪?解决方...

问题描述:

你能告诉我如何读取我的 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_datadata_files您的setup.py.

1)使用pkg_resourcesfrom 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.pyMANIFEST包含任何静态文件。

  • 您也可以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

现在,包装已经完毕,进入阅读部分...

推荐:

使用标准库pkgutilAPI。库代码如下所示:

# 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_binaryimportlib.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_binaryread_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.resourcesPython-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)

奇怪的“总是使用斜线”部分来自setuptoolsAPI

还要注意,如果你使用路径,你必须使用正斜杠 (/) 作为路径分隔符,即使你在 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.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

然而,__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或许能够正确完成这项工作。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用