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

108

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

Моя ситуация

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

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

package\templates\temp_file

Какая-то манипуляция с путями? Отслеживание базового пути пакета?

Ронсон
источник
1
возможный дубликат поиска файла в дистрибутиве модуля Python
Андреас Юнг
возможный дубликат данных доступа Python в
подкаталоге

Ответы:

-13

[добавлено 15.06.2016: очевидно, это работает не во всех ситуациях. пожалуйста, обратитесь к другим ответам]


import os, mypackage
template = os.path.join(mypackage.__path__[0], 'templates', 'temp_file')
jcomeau_ictx
источник
177

TL; DR; Используйте importlib.resourcesмодуль стандартной библиотеки, как описано в методе № 2 ниже.

Традиционная pkg_resourcesотsetuptools не рекомендуется больше , потому что новый метод:

  • он значительно более производительный ;
  • это безопаснее, так как использование пакетов (вместо указателей пути) вызывает ошибки времени компиляции;
  • это более интуитивно понятно, потому что вам не нужно «соединять» пути;
  • это быстрее при разработке, так как вам не нужна дополнительная зависимость ( setuptools), а полагайтесь только на стандартную библиотеку Python.

Я сохранил традиционное перечисление первым, чтобы объяснить различия с новым методом при переносе существующего кода (перенос также объясняется здесь ).



Предположим, что ваши шаблоны находятся в папке, вложенной в пакет вашего модуля:

  <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из 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_resourcesdocs, вы не должны использовать os.path.join:

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

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

2) Python> = 3.7 или с использованием importlib_resourcesбиблиотеки с обратным переносом

Используйте importlib.resourcesмодуль стандартной библиотеки, который более эффективен, чем 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больше НЕ путь, а просто имя файла ресурса, который нужно открыть в существующем пакете; он может не содержать разделителей путей и не иметь подресурсов (т. е. не может быть каталогом).

Для примера, заданного в вопросе, теперь мы должны:

  • превратить <your_package>/templates/ в правильный пакет, создав в нем пустой __init__.pyфайл,
  • так что теперь мы можем использовать простой (возможно, относительный) importоператор (больше не нужно разбирать имена пакетов / модулей),
  • и просто попросите resource_name = "temp_file"(без пути).

Подсказки:

  • Чтобы получить доступ к файлу внутри текущего модуля, установите аргумент пакета __package__, например, pkg_resources.read_text(__package__, 'temp_file')(благодаря @ ben-mares).
  • Все становится интересно, когда запрашивается фактическое имя файлаpath() , поскольку теперь контекстные менеджеры используются для временно созданных файлов (прочтите это ).
  • Добавьте библиотеку с install_requires=[" importlib_resources ; python_version<'3.7'"]обратным переносом, условно для старых Pythons, с помощью (отметьте это, если вы упаковываете свой проект с setuptools<36.2.1).
  • Не забудьте удалить setuptoolsбиблиотеку из требований среды выполнения , если вы перешли с традиционного метода.
  • Не забудьте настроить setup.pyили MANIFESTчтобы включить любые статические файлы .
  • Вы также можете установить zip_safe=Trueв своем setup.py.
Анкостис
источник
1
str.join принимает последовательность resource_path = '/'.join(('templates', 'temp_file'))
Alex Punnen
2
У меня NotImplementedError: Can't perform this operation for loaders without 'get_data()'есть идеи?
leoschet
Следует отметить , что importlib.resourcesи pkg_resourcesэто не всегда совместимы . importlib.resourcesработает с добавленными sys.pathzip- pkg_resourcesфайлами, средствами настройки и работает с файлами яиц, которые представляют собой zip-файлы, хранящиеся в каталоге, в который он сам добавляется sys.path. Например sys.path = [..., '.../foo', '.../bar.zip'], яйца входят .../foo, но пакеты bar.zipтакже можно импортировать. Вы не можете использовать pkg_resourcesдля извлечения данных из пакетов в bar.zip. Я не проверял, регистрирует ли setuptools необходимый загрузчик для importlib.resourcesработы с яйцами.
Мартейн Питерс
Требуется ли дополнительная конфигурация setup.py при Package has no locationпоявлении ошибки ?
zygimantus
1
Если вы хотите получить доступ к файлу внутри текущего модуля (а не к подмодулю, как templatesв примере), вы можете установить packageаргумент __package__, например,pkg_resources.read_text(__package__, 'temp_file')
Бен Марес
46

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

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

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

.
├── 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, поэзия, которые по умолчанию будут включать файлы данных пакета. Итак, если вы используете pyproject.tomlи у вас нет setup.pyфайла, вы можете игнорировать все, что связано с MANIFEST.in.

Теперь, когда упаковка убрана, перейдем к читающей части ...

Рекомендация:

Используйте стандартные библиотечные pkgutilAPI. В коде библиотеки это будет выглядеть так:

# within package/mymodule1.py, for example
import pkgutil

data = pkgutil.get_data(__name__, "templates/temp_file")

Работает на молнии. Он работает на Python 2 и Python 3. Не требует сторонних зависимостей. Я не особо осведомлен о каких-либо недостатках (если да, то прокомментируйте ответ).

Плохие способы избежать:

Плохой способ no1: использование относительных путей из исходного файла

В настоящее время это принятый ответ. В лучшем случае это выглядит примерно так:

from pathlib import Path

resource_path = Path(__file__).parent / "templates"
data = resource_path.joinpath("temp_file").read_bytes()

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

Плохой способ no 2: использование API pkg_resources

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

from pkg_resources import resource_string

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

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

Плохой способ no 3: использование API importlib.resources

В настоящее время это рекомендация в ответе, получившем наибольшее количество голосов. Это недавнее добавление стандартной библиотеки ( новое в Python 3.7 ). Выглядит это так:

from importlib.resources import read_binary

data = read_binary("package.templates", "temp_file")

Что случилось с этим? Ну, к сожалению, не работает ... пока. Это все еще неполный API, для использования importlib.resourcesпотребуется добавить пустой файл templates/__init__.py, чтобы файлы данных находились внутри подпакета, а не в подкаталоге. Он также package/templatesраскроет подкаталог как самостоятельный импортируемый подпакет package.templates. Если это не имеет большого значения и вас это не беспокоит, вы можете продолжить и добавить __init__.pyтуда файл и использовать систему импорта для доступа к ресурсам. Однако, пока вы занимаетесь этим, вы также можете превратить его в my_resources.pyфайл и просто определить некоторые байты или строковые переменные в модуле, а затем импортировать их в код Python. В любом случае, это система импорта, которая делает здесь тяжелую работу.

Достойное упоминание: использование новых API importlib_resources

Об этом еще не упоминалось ни в каких других ответах, но importlib_resourcesэто больше, чем простой бэкпорт importlib.resourcesкода Python 3.7+ . У него есть проходимые API, которые вы можете использовать следующим образом:

import importlib_resources

my_resources = importlib_resources.files("package")
data = (my_resources / "templates" / "temp_file").read_bytes()

Это работает на Python 2 и 3, работает в zip- __init__.pyархивах и не требует добавления ложных файлов в подкаталоги ресурсов. Единственный недостаток, pkgutilкоторый я вижу, заключается в том, что эти новые API еще не появились в stdlib, поэтому все еще существует сторонняя зависимость. Новые API-интерфейсы importlib_resourcesдолжны поступать в stdlib importlib.resourcesв Python 3.9.

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

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

$ pip install resources-example
$ resources-example

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

слабак
источник
1
Он был отредактирован в мае прошлого года. Но я думаю, легко пропустить объяснения во вступлении. Тем не менее, вы советуете людям не
придерживаться
1
@ankostis Позвольте мне вместо этого задать вам вопрос, почему вы рекомендуете, importlib.resourcesнесмотря на все эти недостатки, неполный API, который уже ожидает прекращения поддержки ? Новее не обязательно лучше. Скажите, какие преимущества он на самом деле предлагает по сравнению с stdlib pkgutil, о котором в вашем ответе не упоминается?
wim
1
Уважаемый @wim, последний ответ Бретта Кэнона об использовании pkgutil.get_data()API подтвердил мое чутье - это неразвитый API, который не рекомендуется использовать. Тем не менее, я согласен с вами, importlib.resourcesэто не намного лучшая альтернатива, но до тех пор, пока PY3.10 не решит это, я придерживаюсь этого выбора, он узнал, что это не просто еще один «стандарт», рекомендованный документами.
ankostis
1
@ankostis Я бы отнесся к комментариям Бретта с недоверием. pkgutilвообще не упоминается в графике устаревания PEP 594 - Удаление разряженных батарей из стандартной библиотеки и вряд ли будет удалено без уважительной причины. Он существует с Python 2.3 и указан как часть протокола загрузчика в PEP 302 . Использование «недоопределенного API» - не очень убедительный ответ, который может описать большую часть стандартной библиотеки Python!
wim
2
Позвольте мне добавить: я тоже хочу, чтобы ресурсы importlib преуспели! Я за строго определенные API. Просто в нынешнем состоянии это особо не рекомендуется. API все еще претерпевает изменения, он непригоден для использования во многих существующих пакетах и ​​доступен только в относительно недавних выпусках Python. На практике это хуже, чем практически pkgutilво всех отношениях. Ваше "чутье" и обращение к авторитету для меня бессмысленно, если есть проблемы с get_dataзагрузчиками, то покажите доказательства и практические примеры.
wim
14

Если у вас есть эта структура

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

Если вам интересно, где находится документация:

Мартин Тома
источник
Спасибо за лаконичный ответ
Паоло
pkg_resourcesимеет накладные расходы, которые pkgutilпреодолевают. Кроме того, если предоставленный код запускается как точка входа, __name__будет оцениваться значение __main__, а не имя пакета.
А. Хендри
8

Содержимое в «10.8. Чтение файлов данных в пакете» Python Cookbook, третье издание Дэвида Бизли и Брайана К. Джонса дает ответы.

Я просто доставлю это сюда:

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

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.

Чаокунян
источник
Мне нравится, что вы сослались на поваренную книгу!
А. Хендри
-1

Каждый модуль 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.

Заур Насибов
источник
1
Это не будет работать с исходным кодом, содержащимся в zip-файлах.
А. Хендри
-1

Принятый ответ следует использовать 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 docs ).

Например, со структурой пакета:

+-- 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!'
А. Хендри
источник
Этот ответ неверен - каталог, содержащий ресурсы, не обязательно должен быть пакетом. Это может быть подкаталог внутри пакета. Ограничение importlib.resources, которого pkgutilнет, заключалось в том, что каталог, содержащий ресурсы, __init__.pyтоже должен иметь свой , т.е. он должен быть подпакетом . Это не связано с проблемами пакета пространства имен, которые касаются того, существует ли __init__.pyкаталог верхнего уровня, а не подкаталоги данных внутри пакета.
wim
@wim Извините, но я считаю, что вы ошибаетесь. pre-Python 3.3+, все пакеты должны были __init__.pyбыть загружены. После 3.3 пакетам они не нужны. Пакеты без __init__.pyявляются namespace packages. Согласно pkgutilдокументации, если вы попытаетесь загрузить ресурс из пакета пространства имен, вы получите None. См. Мой обновленный отредактированный ответ.
А. Хендри
Вы использовали pkgutilнеправильно. Попробуйте сpkgutil.get_data("foo", "bar/hi.txt")
wim
-3

предполагая, что вы используете файл с яйцом; не извлекается:

Я «решил» это в недавнем проекте, используя сценарий postinstall, который извлекает мои шаблоны из яйца (zip-файла) в соответствующий каталог в файловой системе. Это было самое быстрое и самое надежное решение, которое я нашел, поскольку работа с ним __path__[0]иногда может пойти не так (я не помню название, но я наткнулся на хотя бы одну библиотеку, которая добавила что-то перед этим списком!).

Также файлы яиц обычно извлекаются «на лету» во временное место, называемое «кэш яиц». Вы можете изменить это местоположение с помощью переменной окружения либо перед запуском вашего скрипта, либо даже позже, например.

os.environ['PYTHON_EGG_CACHE'] = path

Однако есть pkg_resources, который может правильно выполнить эту работу.

Флориан
источник