Як прочитати (статичний) файл із пакета Python?


106

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

Моя ситуація

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

Уявіть, що я хочу прочитати файл із:

package\templates\temp_file

Якась маніпуляція з доріжкою? Відстеження базового шляху пакета?



Відповіді:


-12

[додано 15.06.2016: очевидно, це працює не у всіх ситуаціях. будь ласка, зверніться до інших відповідей]


import os, mypackage
template = os.path.join(mypackage.__path__[0], 'templates', 'temp_file')

175

TLDR; Використовуйте 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'"](перевірте це, якщо ви впакуєте проект setuptools<36.2.1).
  • Не забудьте видалити setuptoolsбібліотеку зі своїх вимог виконання , якщо ви перейшли з традиційного методу.
  • Чи не забудьте налаштувати setup.pyабо MANIFESTщоб включити будь-які статичні файли .
  • Ви також можете встановити zip_safe=Trueу своєму setup.py.

1
str.join приймає послідовність resource_path = '/'.join(('templates', 'temp_file'))
Алекс Пуннен

1
Я продовжую отримувати NotImplementedError: Can't perform this operation for loaders without 'get_data()'якісь ідеї?
leoschet

Слід зазначити , що importlib.resourcesі pkg_resourcesце не завжди сумісні . importlib.resourcesпрацює з доданими до sys.pathzipfiles файлами, setuptools та pkg_resourcesпрацює з файлами яєць, які є zipfiles, що зберігаються в каталозі, до якого додано сам sys.path. Наприклад sys.path = [..., '.../foo', '.../bar.zip'], яйця надходять .../foo, але упаковки bar.zipможна також імпортувати. Ви не можете використовувати pkg_resourcesдля отримання даних з пакетів у bar.zip. Я не перевіряв, чи налаштовує setuptools необхідний завантажувач для importlib.resourcesроботи з яйцями.
Martijn Pieters

Чи потрібна додаткова конфігурація setup.py, якщо Package has no locationз’являється помилка ?
зигімантус

1
Якщо ви хочете отримати доступ до файлу всередині поточного модуля (а не підмодуля, як templatesу прикладі), тоді ви можете встановити packageаргумент __package__, наприкладpkg_resources.read_text(__package__, 'temp_file')
Ben Mares

42

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

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

Структуруйте свій проект таким чином, вводячи файли даних у підкаталог всередині пакета:

.
├── 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")
print("data:", repr(data))
text = pkgutil.get_data(__name__, "templates/temp_file").decode()
print("text:", repr(text))

Працює на блискавках. Він працює на Python 2 та Python 3. Він не вимагає сторонніх залежностей. Я не знаю жодних недоліків (якщо ви є, то, будь ласка, прокоментуйте відповідь).

Погані способи уникнути:

Поганий шлях №1: використання відносних шляхів до вихідного файлу

Наразі це прийнята відповідь. У кращому випадку це виглядає приблизно так:

from pathlib import Path

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

Що з цим? Припущення про наявність у вас файлів та підкаталогів є невірним. Цей підхід не працює, якщо виконання коду, упакованого в поштовий індекс або колесо, і він може повністю вийти з-під контролю користувача, незалежно від того, отриманий ваш пакет у файлову систему.

Поганий спосіб №2: використання API-файлів pkg_resources

Про це йдеться у відповіді на голоси. Це виглядає приблизно так:

from pkg_resources import resource_string

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

Що з цим? Він додає залежність часу виконання від setuptools , яка, як правило , повинна бути лише залежною від часу встановлення . Імпорт та використання pkg_resourcesможуть стати дуже повільними, оскільки код створює робочий набір усіх встановлених пакетів, навіть якщо вас цікавили лише ваші власні ресурси пакету. Це не є великою справою під час встановлення (оскільки установка одноразова), але це некрасиво під час виконання.

Поганий спосіб №3: використання API importlib.resources

Наразі це рекомендація у відповіді на голоси. Це нещодавнє стандартне доповнення бібліотеки ( нове в Python 3.7 ), але є і резервний порт. Це виглядає приблизно так:

try:
    from importlib.resources import read_binary
    from importlib.resources import read_text
except ImportError:
    # Python 2.x backport
    from importlib_resources import read_binary
    from importlib_resources import read_text

data = read_binary("package.templates", "temp_file")
print("data", repr(data))
text = read_text("package.templates", "temp_file")
print("text", repr(text))

Що з цим? Ну, на жаль, це не працює ... поки що. Це все ще неповний API, використання якого importlib.resourcesвимагатиме від вас додати порожній файл templates/__init__.pyдля того, щоб файли даних перебували в підпакеті, а не в підкаталозі. Він також розкриє цей package/templatesпідкаталог як самостійно важливий package.templatesпідпакет. Якщо це не велика справа, і це вас не турбує, тоді ви можете продовжувати додавати __init__.pyфайл і використовувати систему імпорту для доступу до ресурсів. Однак, хоч ви знаходитесь на ньому, ви можете my_resources.pyзамість цього перетворити його у файл, а також просто визначити кілька модулів байтів або рядків у модулі, а потім імпортувати їх у код Python. Це система імпорту в будь-якому випадку робить важкий підйом.

Приклад проекту:

Я створив приклад проекту на github і завантажив на PyPI , де демонструються всі чотири підходи, обговорені вище. Спробуйте це:

$ pip install resources-example
$ resources-example

Для отримання додаткової інформації див. Https://github.com/wimglenn/resources-example .


1
Він був відредагований у травні минулого року. Але я здогадуюсь, що легко пропустити пояснення на вступі. Тим не менш, ти
радиш

1
@ankostis Дозвольте замість цього звернутись до вас, чому б ви рекомендували, importlib.resourcesнезважаючи на всі ці недоліки, із неповним API, який уже очікує анулювання ? Новіше не обов’язково краще. Скажіть, які переваги він насправді пропонує над stdlib pkgutil, про який ваша відповідь не згадує?
Вім

1
Шановний @wim, остання відповідь Бретта Канона щодо використання pkgutil.get_data()підтвердила моє відчуття кишечника - це недостатньо розвинений, застарілий API. Сказане, я згоден з вами, importlib.resourcesне є набагато кращою альтернативою, але поки PY3.10 не вирішить це питання, я стою на цьому виборі, дізнавшись, що це не просто ще один "стандарт", який рекомендують документи.
ankostis

1
@ankostis Я б взяв коментарі Бретта із зерном солі. pkgutilвзагалі не згадується в графіку депресії PEP 594 - Вилучення мертвих батарей із стандартної бібліотеки , і навряд чи їх буде вилучено без поважних причин. Це було ще з часів Python 2.3 і визначено як частину протоколу завантажувача в PEP 302 . Використання "недостатньо визначеного API" - не дуже переконлива відповідь, яка могла б описати більшість стандартних бібліотек Python!
Вім

2
Дозвольте додати: я також хочу, щоб ресурси importlib були успішними! Я все для чітко визначених API. Просто в його нинішньому стані це реально не рекомендується. API все ще зазнає змін, він непридатний для багатьох існуючих пакетів і доступний лише у відносно останніх випусках Python. На практиці це гірше, ніж pkgutilпрактично всіляко. Ваше "почуття кишки" та звернення до авторитету для мене безглуздо, якщо є проблеми з get_dataвантажниками, то покажіть докази та практичні приклади.
Вім

15

Якщо у вас є така структура

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

Якщо вам цікаво, де знаходиться документація:


Дякую за стисну відповідь
Паоло

8

Зміст у "10.8. Читання файлів даних всередині пакета" з кухонної книги Python, Третє видання Девід Бізлі та Брайан К. Джонс, які дають відповіді.

Я просто доїду сюди:

Припустимо, у вас є пакет із файлами, організованими так:

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.



-2

припускаючи, що ви використовуєте файл яєць; не добувається:

Я "вирішив" це в недавньому проекті, використовуючи сценарій після встановлення, який витягує мої шаблони з яйця (zip-файлу) у відповідний каталог файлової системи. Це було найшвидше, найнадійніше рішення, яке я знайшов, оскільки робота з часом __path__[0]може піти не так (я не пригадую ім'я, але я переглядаю принаймні одну бібліотеку, яка щось додала перед цим списком!).

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

os.environ['PYTHON_EGG_CACHE'] = path

Однак є pkg_resources, які можуть виконати цю роботу належним чином.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.