Вопрос-Ответ

How to read a (static) file from inside a Python package?

Как прочитать (статический) файл из пакета Python?

Не могли бы вы сказать мне, как я могу прочитать файл, который находится внутри моего пакета Python?

Моя ситуация

Загружаемый мной пакет содержит ряд шаблонов (текстовые файлы, используемые в качестве строк), которые я хочу загрузить из программы. Но как мне указать путь к такому файлу?

Представьте, что я хочу прочитать файл из:

package\templates\temp_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 from 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 и / или использовать долгожданный zipapp упаковщик из python-3.5 для создания автономных дистрибутивов.



  • Не забудьте добавить setuptools в ваши требования во время выполнения (например, в install_requires`).




... и обратите внимание, что согласно Setuptools /pkg_resources docs, вы не должны использовать os.path.join:


Базовый доступ к ресурсам


Обратите внимание, что имена ресурсов должны представлять собой пути, /разделенные путями, и не могут быть абсолютными (т. Е. без начальных /) или содержать относительные имена, такие как "..". Не используйте os.path подпрограммы для манипулирования путями ресурсов, поскольку они не являются путями файловой системы.


2) Python >= 3.7 или с использованием backported 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 значение __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

Прелюдия к упаковке:

Прежде чем вы начнете беспокоиться о чтении файлов ресурсов, первым шагом будет убедиться, что файлы данных изначально упаковываются в ваш дистрибутив - их легко прочитать непосредственно из дерева исходных текстов, но важной частью является обеспечение доступа к этим файлам ресурсов из кода в установленном пакете.

Структурируйте свой проект следующим образом, помещая файлы данных в подкаталог внутри пакета:

.
├── package
│   ├── __init__.py
│   ├── templates
│   │   └── temp_file
│   ├── 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: использование API pkg_resources

Это описано в ответе, набравшем наибольшее количество голосов. Это выглядит примерно так:

from pkg_resources import resource_string

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

Что в этом плохого? Он добавляет зависимость времени выполнения от setuptools, которая предпочтительно должна зависеть только от времени установки. Импорт и использование pkg_resources может стать очень медленным, поскольку код создает рабочий набор из всех установленных пакетов, даже если вас интересовали только ваши собственные ресурсы пакета. Это не имеет большого значения во время установки (поскольку установка одноразовая), но это некрасиво во время выполнения.

Плохой способ № 3: использование устаревших API importlib.resources

Это в настоящее время ранее было рекомендацией ответа, набравшего наибольшее количество голосов. Это находится в стандартной библиотеке начиная с 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 файлы повсюду, размывая границу между данными и кодом.

Этот подход был устарел в upstream importlib_resources в 2021 году и устарел в stdlib версии Python 3.11. bpo-45514 отслеживал устаревание и при переходе с устаревших предложений _legacy.py оболочек для облегчения перехода.

Почетное упоминание: использование проходимого API ресурсов importlib

Это не упоминалось в ответе, набравшем наибольшее количество голосов, когда я публиковал об этом (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 доступны только в stdlib importlib.resources из Python-3.9+, поэтому для поддержки старых версий Python все еще требуется зависимость от сторонних производителей. Если вам нужно работать только на Python-3.9+, тогда используйте этот подход, или вы можете добавить уровень совместимости и условную зависимость от backport для более старых версий 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 не истечет, моя рекомендация остается stdlib pkgutil, чтобы избежать дополнительной сложности условной зависимости.

Пример проекта:

Я создал пример проекта на GitHub и загрузил на PyPI, который демонстрирует все пять подходов, рассмотренных выше. Попробуйте с помощью:

$ pip install resources-example
$ resources-example

Смотрите https://github.com/wimglenn/resources-example для получения дополнительной информации.

Ответ 3

Содержимое раздела "10.8. Чтение файлов данных внутри пакета" Поваренной книги Python, третье издание Дэвида Бизли и Брайана К. Ответы дает Джонс.

Я просто приведу его сюда:

Предположим, у вас есть пакет с файлами, организованными следующим образом:

mypackage/
__init__.py
somedata.dat
spam.py

Теперь предположим, что файл spam.py хочет прочитать содержимое файла somedata.dat. Чтобы сделать
это, используйте следующий код:

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

Результирующие переменные data будут представлять собой строку байтов, содержащую исходное содержимое файла.

Первый аргумент get_data() - это строка, содержащая имя пакета. Вы можете либо указать его напрямую, либо использовать специальную переменную, такую как __package__. Второй аргумент - это относительное имя файла в пакете. При необходимости вы можете перемещаться по разным каталогам, используя стандартные соглашения Unix об именах файлов, при условии, что конечный каталог по-прежнему находится в пакете.

Таким образом, пакет может быть установлен как directory, .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 автоматически преобразует косые черты в соответствующие разделители для конкретной платформы во время сборки


На случай, если вам интересно, где документация:

python file