Как прочитать (статический) файл из пакета 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 автоматически преобразует косые черты в соответствующие разделители для конкретной платформы во время сборки
На случай, если вам интересно, где документация: