Bagaimana cara membaca file (statis) dari dalam paket Python?


107

Bisakah Anda memberi tahu saya bagaimana saya bisa membaca file yang ada di dalam paket Python saya?

Situasi saya

Paket yang saya muat memiliki sejumlah template (file teks yang digunakan sebagai string) yang ingin saya muat dari dalam program. Tetapi bagaimana cara menentukan jalur ke file tersebut?

Bayangkan saya ingin membaca file dari:

package\templates\temp_file

Semacam manipulasi jalan? Pelacakan jalur dasar paket?



Jawaban:


-13

[ditambahkan 2016-06-15: tampaknya ini tidak berfungsi di semua situasi. silakan lihat jawaban lainnya]


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

176

TLDR; Gunakan importlib.resourcesmodul pustaka standar seperti yang dijelaskan dalam metode no 2, di bawah ini.

The tradisional pkg_resourcesdarisetuptools tidak dianjurkan lagi karena metode baru:

  • secara signifikan lebih berkinerja ;
  • lebih aman karena penggunaan paket (daripada path-stings) menimbulkan kesalahan waktu kompilasi;
  • ini lebih intuitif karena Anda tidak harus "bergabung" dengan jalur;
  • itu lebih cepat ketika mengembangkan karena Anda tidak memerlukan ketergantungan tambahan ( setuptools), tetapi hanya mengandalkan pustaka standar Python.

Saya menyimpan yang tradisional terlebih dahulu, untuk menjelaskan perbedaan dengan metode baru saat mem-porting kode yang ada (porting juga dijelaskan di sini ).



Mari asumsikan template Anda berada di folder yang bersarang di dalam paket modul Anda:

  <your-package>
    +--<module-asking-the-file>
    +--templates/
          +--temp_file                         <-- We want this file.

Catatan 1: Yang pasti, kita TIDAK boleh mengutak-atik __file__atribut (misalnya kode akan rusak saat disajikan dari zip).

Catatan 2: Jika Anda membuat paket ini, ingatlah untuk mendeklarasikan file data Anda sebagai package_dataataudata_files di file setup.py.

1) Menggunakan pkg_resourcesdari setuptools(lambat)

Anda dapat menggunakan pkg_resourcespaket dari distribusi setuptools , tetapi itu datang dengan biaya, berdasarkan kinerja :

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)

Tips:

  • Hal ini akan membaca data bahkan jika distribusi Anda zip, sehingga Anda dapat mengatur zip_safe=Truedi Anda setup.py, dan / atau menggunakan lama ditunggu-tunggu zipapppacker dari python-3.5 untuk membuat distribusi mandiri.

  • Ingatlah untuk menambahkan setuptoolspersyaratan run-time Anda (mis. Di install_requires`).

... dan perhatikan bahwa menurut Setuptools / pkg_resourcesdocs, Anda tidak boleh menggunakan os.path.join:

Akses Sumber Daya Dasar

Perhatikan bahwa nama sumber daya harus merupakan /jalur yang dipisahkan dan tidak boleh absolut (yaitu tanpa awalan /) atau berisi nama relatif seperti " ..". Jangan tidak menggunakan os.pathrutinitas untuk memanipulasi jalur sumber daya, karena mereka tidak jalan filesystem.

2) Python> = 3.7, atau menggunakan importlib_resourcesperpustakaan backport

Gunakan importlib.resourcesmodul perpustakaan standar yang lebih efisien daripada di setuptoolsatas:

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')

Perhatian:

Mengenai fungsinya read_text(package, resource):

  • The packagedapat berupa string atau modul.
  • Ini resourceBUKAN path lagi, tetapi hanya nama file dari sumber daya yang akan dibuka, dalam paket yang sudah ada; itu mungkin tidak berisi pemisah jalur dan mungkin tidak memiliki sub-sumber daya (yaitu tidak bisa menjadi direktori).

Untuk contoh yang ditanyakan dalam pertanyaan, sekarang kita harus:

  • buat <your_package>/templates/ menjadi paket yang tepat, dengan membuat __init__.pyfile kosong di dalamnya,
  • jadi sekarang kita bisa menggunakan pernyataan sederhana (mungkin relatif) import(tidak perlu lagi mem-parsing nama paket / modul),
  • dan hanya meminta resource_name = "temp_file"(tidak ada jalan).

Tips:

  • Untuk mengakses file di dalam modul saat ini, setel argumen paket ke __package__, misalnya pkg_resources.read_text(__package__, 'temp_file')(terima kasih kepada @ ben-mares).
  • Hal-hal menjadi menarik ketika nama file yang sebenarnya diminta path(), karena sekarang manajer konteks digunakan untuk file yang dibuat sementara (baca ini ).
  • Tambahkan pustaka yang di-backport, secara kondisional untuk Pythons yang lebih lama, dengan install_requires=[" importlib_resources ; python_version<'3.7'"](centang ini jika Anda mengemas proyek Anda dengan setuptools<36.2.1).
  • Ingatlah untuk menghapus setuptoolspustaka dari persyaratan waktu proses Anda , jika Anda bermigrasi dari metode tradisional.
  • Ingatlah untuk menyesuaikan setup.pyatau MANIFESTuntuk menyertakan file statis .
  • Anda juga dapat mengatur zip_safe=Truedi setup.py.

1
str.join mengambil urutan resource_path = '/'.join(('templates', 'temp_file'))
Alex Punnen

1
Saya terus mendapatkan NotImplementedError: Can't perform this operation for loaders without 'get_data()'ide?
leoschet

Perhatikan bahwa importlib.resourcesdan pkg_resourcesyang tidak selalu kompatibel . importlib.resourcesberfungsi dengan zipfiles yang ditambahkan ke sys.path, setuptools dan pkg_resourcesbekerja dengan file telur, yang merupakan file zip yang disimpan dalam direktori yang ditambahkan ke dalamnya sys.path. Misal dengan sys.path = [..., '.../foo', '.../bar.zip'], telur masuk .../foo, tapi paket masuk bar.zipjuga bisa diimpor. Anda tidak dapat menggunakan pkg_resourcesuntuk mengekstrak data dari paket dalam bar.zip. Saya belum memeriksa apakah setuptools mendaftarkan loader yang diperlukan untuk importlib.resourcesbekerja dengan telur.
Martijn Pieters

Apakah konfigurasi tambahan setup.py diperlukan jika kesalahan Package has no locationmuncul?
zygimantus

1
Jika Anda ingin mengakses file di dalam modul saat ini (dan bukan submodul seperti templatespada contoh), maka Anda dapat mengatur packageargumen ke __package__, misalnyapkg_resources.read_text(__package__, 'temp_file')
Ben Mares

43

Awal pengemasan:

Sebelum Anda bahkan khawatir tentang membaca file sumber daya, langkah pertama adalah memastikan bahwa file data telah dikemas ke dalam distribusi Anda - mudah untuk membacanya langsung dari struktur pohon sumber, tetapi bagian yang penting adalah membuat pastikan file sumber daya ini dapat diakses dari kode dalam paket yang diinstal .

Susun proyek Anda seperti ini, letakkan file data ke dalam subdirektori di dalam paket:

.
├── package
   ├── __init__.py
   ├── templates
      └── temp_file
   ├── mymodule1.py
   └── mymodule2.py
├── README.rst
├── MANIFEST.in
└── setup.py

Anda harus lulus include_package_data=Truedalam setup()panggilan. File manifes hanya diperlukan jika Anda ingin menggunakan setuptools / distutils dan membangun distribusi sumber. Untuk memastikan templates/temp_filepaket tersebut dikemas untuk contoh struktur proyek ini, tambahkan baris seperti ini ke dalam file manifes:

recursive-include package *

Catatan penting historis: Menggunakan file manifes tidak diperlukan untuk backend build modern seperti flit, poetry, yang akan menyertakan file data paket secara default. Jadi, jika Anda menggunakan pyproject.tomldan tidak memiliki setup.pyfile maka Anda dapat mengabaikan semua hal tentang MANIFEST.in.

Sekarang, dengan mengemasnya, ke bagian bacaan ...

Rekomendasi:

Gunakan pkgutilAPI perpustakaan standar . Ini akan terlihat seperti ini di kode perpustakaan:

# 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))

Ini berfungsi dalam ritsleting. Ia bekerja pada Python 2 dan Python 3. Ia tidak membutuhkan ketergantungan pihak ketiga. Saya tidak benar-benar mengetahui kerugian apa pun (jika ya, silakan komentari jawabannya).

Cara buruk untuk menghindari:

Cara buruk # 1: menggunakan jalur relatif dari file sumber

Saat ini adalah jawaban yang diterima. Paling banter, tampilannya seperti ini:

from pathlib import Path

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

Apa yang salah dengan itu? Asumsi bahwa Anda memiliki file dan subdirektori tidak benar. Pendekatan ini tidak berfungsi jika menjalankan kode yang dikemas dalam zip atau roda, dan mungkin sepenuhnya di luar kendali pengguna apakah paket Anda diekstrak ke sistem file atau tidak.

Cara buruk # 2: menggunakan pkg_resources API

Ini dijelaskan dalam jawaban pilihan teratas. Ini terlihat seperti ini:

from pkg_resources import resource_string

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

Apa yang salah dengan itu? Ini menambahkan dependensi runtime pada setuptools , yang seharusnya hanya dependensi waktu instal . Mengimpor dan menggunakan pkg_resourcesbisa menjadi sangat lambat, karena kode membangun satu set yang berfungsi dari semua paket yang diinstal, meskipun Anda hanya tertarik pada sumber paket Anda sendiri . Itu bukan masalah besar pada waktu penginstalan (karena penginstalan hanya sekali), tetapi jelek saat runtime.

Cara buruk # 3: Menggunakan API importlib.resources

Saat ini, ini adalah rekomendasi dalam jawaban pilihan teratas. Ini adalah tambahan pustaka standar baru-baru ini ( baru di Python 3.7 ), tetapi ada backport yang tersedia juga. Ini terlihat seperti ini:

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))

Apa yang salah dengan itu? Sayangnya, itu belum berhasil ... Ini masih merupakan API yang tidak lengkap, penggunaan importlib.resourcesakan mengharuskan Anda untuk menambahkan file kosong templates/__init__.pyagar file data akan berada di dalam sub-paket daripada di subdirektori. Ini juga akan mengekspos package/templatessubdirektori sebagai package.templatessub-paket yang dapat diimpor dengan sendirinya. Jika itu bukan masalah besar dan itu tidak mengganggu Anda, Anda dapat melanjutkan dan menambahkan __init__.pyfile di sana dan menggunakan sistem impor untuk mengakses sumber daya. Namun, saat Anda melakukannya, Anda dapat membuatnya menjadi my_resources.pyfile, dan cukup mendefinisikan beberapa byte atau variabel string dalam modul, lalu mengimpornya dalam kode Python. Ini adalah sistem impor yang melakukan pekerjaan berat di sini.

Contoh proyek:

Saya telah membuat proyek contoh di github dan mengunggahnya di PyPI , yang menunjukkan keempat pendekatan yang dibahas di atas. Cobalah dengan:

$ pip install resources-example
$ resources-example

Lihat https://github.com/wimglenn/resources-example untuk info lebih lanjut.


1
Ini telah diedit Mei lalu. Tapi saya rasa mudah untuk melewatkan penjelasan di intro. Namun, Anda menasihati orang-orang agar tidak mengikuti standar - itu peluru yang sulit untuk digigit :-)
ankostis

1
@ankostis Izinkan saya mengalihkan pertanyaan kepada Anda, mengapa Anda merekomendasikan importlib.resourcesmeskipun semua kekurangan ini dengan API yang tidak lengkap yang sudah menunggu penghentian ? Lebih baru belum tentu lebih baik. Beri tahu saya keuntungan apa yang sebenarnya ditawarkan dibandingkan stdlib pkgutil, yang tidak disebutkan dalam jawaban Anda?
wim

1
@Wim yang terhormat, tanggapan terakhir Brett Canon tentang penggunaan pkgutil.get_data()mengkonfirmasi firasat saya - ini adalah API yang belum berkembang dan harus ditinggalkan. Yang mengatakan, saya setuju dengan Anda, importlib.resourcesbukanlah alternatif yang jauh lebih baik, tetapi sampai PY3.10 menyelesaikan ini, saya mendukung pilihan ini, heving belajar bahwa ini bukan hanya "standar" lain yang direkomendasikan oleh dokumen.
ankostis

1
@ankostis Saya akan menerima komentar Brett dengan sebutir garam. pkgutiltidak disebutkan sama sekali pada jadwal penghentian PEP 594 - Melepaskan baterai mati dari pustaka standar , dan tidak mungkin dilepas tanpa alasan yang kuat. Itu sudah ada sejak Python 2.3 dan ditetapkan sebagai bagian dari protokol loader di PEP 302 . Menggunakan "API yang tidak didefinisikan" bukanlah jawaban yang sangat meyakinkan, yang bisa menggambarkan sebagian besar pustaka standar Python!
wim

2
Izinkan saya menambahkan: Saya ingin melihat resource importlib berhasil juga! Saya mendukung API yang didefinisikan secara ketat. Hanya saja dalam keadaannya saat ini, tidak bisa benar-benar direkomendasikan. API masih mengalami perubahan, tidak dapat digunakan untuk banyak paket yang ada, dan hanya tersedia dalam rilis Python yang relatif baru. Dalam praktiknya, ini lebih buruk daripada pkgutilhampir semua hal. "Naluri" dan seruan Anda kepada otoritas tidak ada artinya bagi saya, jika ada masalah dengan get_dataloader, tunjukkan bukti dan contoh praktis.
wim

14

Jika Anda memiliki struktur ini

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

Anda membutuhkan kode ini:

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)

Bagian aneh "selalu gunakan garis miring" berasal dari setuptoolsAPI

Perhatikan juga bahwa jika Anda menggunakan jalur, Anda harus menggunakan garis miring (/) sebagai pemisah jalur, meskipun Anda menggunakan Windows. Setuptools secara otomatis mengonversi garis miring menjadi pemisah khusus platform yang sesuai pada waktu pembuatan

Jika Anda bertanya-tanya di mana dokumentasinya:


Terima kasih atas jawaban singkat Anda
Paolo

pkg_resourcesmemiliki overhead yang pkgutilmengatasi. Juga, jika kode yang disediakan dijalankan sebagai titik masuk, __name__akan mengevaluasi ke __main__, bukan nama paket.
A. Hendry

8

Konten dalam "10.8. Membaca Datafiles Dalam Paket" dari Python Cookbook, Edisi Ketiga oleh David Beazley dan Brian K. Jones memberikan jawabannya.

Saya hanya akan membawanya ke sini:

Misalkan Anda memiliki paket dengan file yang diatur sebagai berikut:

mypackage/
    __init__.py
    somedata.dat
    spam.py

Sekarang misalkan file spam.py ingin membaca konten file somedata.dat. Untuk melakukannya, gunakan kode berikut:

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

Data variabel yang dihasilkan akan menjadi string byte yang berisi konten mentah file.

Argumen pertama untuk get_data () adalah string yang berisi nama paket. Anda dapat memasukkannya secara langsung atau menggunakan variabel khusus, seperti __package__. Argumen kedua adalah nama relatif file di dalam paket. Jika perlu, Anda dapat menavigasi ke direktori yang berbeda menggunakan konvensi nama file Unix standar selama direktori terakhir masih berada di dalam paket.

Dengan cara ini, paket dapat diinstal sebagai direktori, .zip atau .egg.


Saya suka Anda mereferensikan buku masak!
A. Hendry

0

Jawaban yang diterima harus digunakan importlib.resources. pkgutil.get_datajuga membutuhkan argumen packagesebagai paket non-namespace ( lihat pkgutil docs ). Karenanya, direktori yang berisi sumber daya harus memiliki __init__.pyfile, sehingga memiliki batasan yang sama persis seperti importlib.resources. Jika masalah overhead pkg_resourcestidak menjadi perhatian, ini juga merupakan alternatif yang dapat diterima.



-3

dengan asumsi Anda menggunakan file telur; tidak diekstrak:

Saya "memecahkan" ini dalam proyek baru-baru ini, dengan menggunakan skrip pasca-instalasi, yang mengekstrak template saya dari telur (file zip) ke direktori yang sesuai di sistem file. Itu adalah solusi tercepat dan paling andal yang saya temukan, karena bekerja dengan __path__[0]terkadang bisa salah (saya tidak ingat namanya, tetapi saya menemukan setidaknya satu perpustakaan, yang menambahkan sesuatu di depan daftar itu!).

File telur juga biasanya diekstrak dengan cepat ke lokasi sementara yang disebut "cache telur". Anda dapat mengubah lokasi itu menggunakan variabel lingkungan, baik sebelum memulai skrip Anda atau bahkan nanti, mis.

os.environ['PYTHON_EGG_CACHE'] = path

Namun ada pkg_resources yang mungkin melakukan pekerjaan dengan benar.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.