Come leggere un file (statico) dall'interno di un pacchetto Python?


107

Puoi dirmi come posso leggere un file che si trova all'interno del mio pacchetto Python?

La mia situazione

Un pacchetto che carico ha una serie di modelli (file di testo usati come stringhe) che voglio caricare dall'interno del programma. Ma come faccio a specificare il percorso di tale file?

Immagina di voler leggere un file da:

package\templates\temp_file

Qualche tipo di manipolazione del percorso? Monitoraggio del percorso di base del pacchetto?



Risposte:


-13

[aggiunto 2016-06-15: apparentemente questo non funziona in tutte le situazioni. fare riferimento alle altre risposte]


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

176

TLDR; Usa il importlib.resourcesmodulo della libreria standard come spiegato nel metodo n. 2, di seguito.

Il tradizionale pkg_resourcesdasetuptools non è più consigliato perché il nuovo metodo:

  • è significativamente più performante ;
  • è più sicuro poiché l'uso di pacchetti (invece di path-stings) solleva errori in fase di compilazione;
  • è più intuitivo perché non devi "unire" percorsi;
  • è più veloce durante lo sviluppo poiché non è necessaria una dipendenza aggiuntiva ( setuptools), ma si fa affidamento solo sulla libreria standard di Python.

Ho mantenuto il tradizionale elencato per primo, per spiegare le differenze con il nuovo metodo durante il porting del codice esistente (porting spiegato anche qui ).



Supponiamo che i tuoi modelli si trovino in una cartella nidificata all'interno del pacchetto del tuo modulo:

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

Nota 1: Di sicuro, NON dovremmo giocherellare con l' __file__attributo (ad esempio, il codice si interromperà se servito da uno zip).

Nota 2: se stai creando questo pacchetto, ricordati di declatre i tuoi file di dati come package_dataodata_files nel tuo setup.py.

1) Utilizzando pkg_resourcesda setuptools(lento)

Puoi usare il pkg_resourcespacchetto dalla distribuzione setuptools , ma ha un costo, in termini di prestazioni :

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)

Suggerimenti:

  • Questo leggerà i dati anche se la tua distribuzione è compressa, quindi puoi impostare zip_safe=Truenel tuo setup.py, e / o usare il tanto atteso zipapppacker da python-3.5 per creare distribuzioni autonome.

  • Ricordati di aggiungere setuptoolsai tuoi requisiti di runtime (ad esempio in install_requires`).

... e nota che secondo Setuptools / pkg_resourcesdocs, non dovresti usare os.path.join:

Accesso alle risorse di base

Tieni presente che i nomi delle risorse devono essere /percorsi separati e non possono essere assoluti (ovvero non iniziali /) o contenere nomi relativi come " ..". Evitare Non usare os.pathle routine per manipolare i percorsi delle risorse, così come sono non i percorsi del file system.

2) Python> = 3.7, o usando la importlib_resourceslibreria con backport

Usa il importlib.resourcesmodulo della libreria standard che è più efficiente di setuptools, sopra:

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

Attenzione:

Per quanto riguarda la funzione read_text(package, resource):

  • La packagepuò essere una stringa o un modulo.
  • Il resourcenon è un percorso più, ma solo il nome del file della risorsa di aprire, all'interno di un pacchetto esistente; potrebbe non contenere separatori di percorso e potrebbe non avere sotto-risorse (cioè non può essere una directory).

Per l'esempio posto nella domanda, ora dobbiamo:

  • trasformalo <your_package>/templates/ in un pacchetto appropriato, creando un __init__.pyfile vuoto al suo interno,
  • quindi ora possiamo usare una semplice importistruzione ( possibilmente relativa) (non è più necessario analizzare i nomi di pacchetti / moduli),
  • e chiedi semplicemente resource_name = "temp_file"(nessun percorso).

Suggerimenti:

  • Per accedere a un file all'interno del modulo corrente, imposta l'argomento del pacchetto su __package__, ad esempio pkg_resources.read_text(__package__, 'temp_file')(grazie a @ ben-mares).
  • Le cose diventano interessanti quando viene chiesto un nome file effettivo con path(), poiché ora i gestori di contesto vengono utilizzati per i file creati temporaneamente (leggi questo ).
  • Aggiungere la libreria backport, condizionalmente per Pythons più anziani, con install_requires=[" importlib_resources ; python_version<'3.7'"](controllare questo se si comprime il vostro progetto con setuptools<36.2.1).
  • Ricorda di rimuovere la setuptoolslibreria dai tuoi requisiti di runtime , se hai migrato dal metodo tradizionale.
  • Ricordatevi di personalizzare setup.pyo MANIFESTper includere tutti i file statici .
  • Puoi anche impostare zip_safe=Truenel tuo file setup.py.

1
str.join prende la sequenza resource_path = '/'.join(('templates', 'temp_file'))
Alex Punnen

1
Continuo a ricevere NotImplementedError: Can't perform this operation for loaders without 'get_data()'idee?
leoschet

Nota che importlib.resourcese nonpkg_resources sono necessariamente compatibili . importlib.resourcesfunziona con file zip aggiunti a sys.path, setuptools e pkg_resourceslavora con file egg, che sono file zip archiviati in una directory a cui viene aggiunto sys.path. Ad esempio sys.path = [..., '.../foo', '.../bar.zip'], con le uova entrano .../foo, ma è bar.zippossibile importare anche i pacchetti . Non puoi utilizzare pkg_resourcesper estrarre dati da pacchetti in bar.zip. Non ho controllato se setuptools registra il caricatore necessario per importlib.resourceslavorare con le uova.
Martijn Pieters

È necessaria una configurazione aggiuntiva di setup.py se viene Package has no locationvisualizzato un errore ?
zygimantus

1
Nel caso in cui desideri accedere a un file all'interno del modulo corrente (e non un sottomodulo come templatesnell'esempio), puoi impostare l' packageargomento su __package__, ad esempiopkg_resources.read_text(__package__, 'temp_file')
Ben Mares,

43

Un preludio al packaging:

Prima ancora che tu possa preoccuparti di leggere i file di risorse, il primo passo è assicurarti che i file di dati vengano inseriti nella tua distribuzione in primo luogo: è facile leggerli direttamente dall'albero dei sorgenti, ma la parte importante è fare assicurarsi che questi file di risorse siano accessibili dal codice all'interno di un pacchetto installato .

Struttura il tuo progetto in questo modo, inserendo i file di dati in una sottodirectory all'interno del pacchetto:

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

Dovresti passare include_package_data=Truela setup()chiamata. Il file manifest è necessario solo se si desidera utilizzare setuptools / distutils e creare distribuzioni di sorgenti. Per assicurarti che templates/temp_filevenga impacchettato per questa struttura di progetto di esempio, aggiungi una riga come questa nel file manifest:

recursive-include package *

Nota storica: l' utilizzo di un file manifest non è necessario per i backend di build moderni come flit, poetry, che includeranno i file di dati del pacchetto per impostazione predefinita. Quindi, se stai usando pyproject.tomle non hai un setup.pyfile, puoi ignorare tutto ciò che riguarda MANIFEST.in.

Ora, con l'imballaggio fuori mano, sulla parte di lettura ...

Raccomandazione:

Usa le pkgutilAPI della libreria standard . Apparirà così nel codice della libreria:

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

Funziona con le zip. Funziona su Python 2 e Python 3. Non richiede dipendenze di terze parti. Non sono realmente a conoscenza di eventuali svantaggi (se lo sei, allora per favore commenta la risposta).

Cattivi modi per evitare:

Modo sbagliato n. 1: utilizzo di percorsi relativi da un file sorgente

Questa è attualmente la risposta accettata. Nella migliore delle ipotesi, assomiglia a questo:

from pathlib import Path

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

Cosa c'è che non va? Il presupposto che siano disponibili file e sottodirectory non è corretto. Questo approccio non funziona se si esegue codice che è impacchettato in uno zip o in una ruota, e potrebbe essere completamente fuori dal controllo dell'utente se il pacchetto viene estratto o meno in un filesystem.

Modo sbagliato n. 2: utilizzo delle API pkg_resources

Questo è descritto nella risposta più votata. Assomiglia a questo:

from pkg_resources import resource_string

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

Cosa c'è che non va? Aggiunge una dipendenza di runtime da setuptools , che dovrebbe essere preferibilmente solo una dipendenza dal tempo di installazione . L'importazione e l'utilizzo pkg_resourcespossono diventare molto lenti, poiché il codice costruisce un set funzionante di tutti i pacchetti installati, anche se eri interessato solo alle risorse del tuo pacchetto. Non è un grosso problema al momento dell'installazione (poiché l'installazione è una tantum), ma è brutto in fase di esecuzione.

Modo sbagliato # 3: utilizzo delle API importlib.resources

Questa è attualmente la raccomandazione nella risposta più votata. È una recente aggiunta alla libreria standard ( nuova in Python 3.7 ), ma è disponibile anche un backport. Assomiglia a questo:

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

Cosa c'è che non va? Ebbene, sfortunatamente, non funziona ... ancora. Questa è ancora un'API incompleta, l'utilizzo importlib.resourcesrichiederà di aggiungere un file vuoto templates/__init__.pyin modo che i file di dati risiedano all'interno di un sottopacchetto piuttosto che in una sottodirectory. Inoltre esporrà la package/templatessottodirectory come un sottopacchetto importabile package.templatesa sé stante. Se questo non è un grosso problema e non ti dà fastidio, puoi andare avanti e aggiungere il __init__.pyfile lì e utilizzare il sistema di importazione per accedere alle risorse. Tuttavia, già che ci sei, potresti anche trasformarlo in un my_resources.pyfile e definire solo alcuni byte o variabili stringa nel modulo, quindi importarli nel codice Python. In entrambi i casi è il sistema di importazione che fa il lavoro pesante.

Progetto di esempio:

Ho creato un progetto di esempio su GitHub e caricato su PyPI , che mostra tutti e quattro gli approcci discussi sopra. Provalo con:

$ pip install resources-example
$ resources-example

Vedi https://github.com/wimglenn/resources-example per maggiori informazioni.


1
È stato modificato lo scorso maggio. Ma immagino che sia facile perdere le spiegazioni nell'introduzione. Tuttavia, consigli le persone contro lo standard: è un proiettile difficile da mordere :-)
ankostis

1
@ankostis Lasciami invece rivolgere la domanda a te, perché mi consiglieresti importlib.resourcesnonostante tutte queste carenze con un'API incompleta che è già in attesa di ritiro ? Più nuovo non è necessariamente migliore. Dimmi quali vantaggi offre effettivamente rispetto allo stdlib pkgutil, di cui la tua risposta non fa alcuna menzione?
wim

1
Caro @wim, l'ultima risposta di Brett Canon sull'uso di ha pkgutil.get_data()confermato la mia sensazione istintiva: è un'API sottosviluppata e da deprecare. Detto questo, sono d'accordo con te, importlib.resourcesnon è un'alternativa molto migliore, ma fino a quando PY3.10 non risolve questo problema, rimango su questa scelta, avendo imparato che non è solo un altro "standard" raccomandato dai documenti.
ankostis

1
@ankostis Prenderei i commenti di Brett con le pinze. pkgutilnon è affatto menzionato nel programma di deprecazione di PEP 594 - Rimozione delle batterie scariche dalla libreria standard ed è improbabile che venga rimosso senza una buona ragione. È in circolazione da Python 2.3 e specificato come parte del protocollo di caricamento in PEP 302 . Usare una "API sotto definita" non è una risposta molto convincente, che potrebbe descrivere la maggior parte della libreria standard di Python!
wim

2
Aggiungo: voglio che anche le risorse importlib abbiano successo! Sono tutto per API rigorosamente definite. È solo che nel suo stato attuale, non può essere davvero raccomandato. L'API è ancora in fase di modifica, è inutilizzabile per molti pacchetti esistenti e disponibile solo nelle versioni di Python relativamente recenti. In pratica è peggio che pkgutilin quasi tutti i modi. Il tuo "istinto" e il tuo appello all'autorità non hanno senso per me, se ci sono problemi con i get_datacaricatori, mostra prove ed esempi pratici.
wim

14

Nel caso tu abbia questa struttura

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

hai bisogno di questo codice:

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)

La strana parte "usa sempre la barra" proviene dalle setuptoolsAPI

Notare inoltre che se si utilizzano percorsi, è necessario utilizzare una barra (/) come separatore di percorso, anche se si utilizza Windows. Setuptools converte automaticamente le barre in separatori specifici della piattaforma appropriati al momento della compilazione

Nel caso ti chiedessi dove sia la documentazione:


Grazie per la tua risposta concisa
Paolo

pkg_resourcesha un sovraccarico che pkgutilsupera. Inoltre, se il codice fornito viene eseguito come punto di ingresso, __name__valuterà __main__, non il nome del pacchetto.
A. Hendry

8

Il contenuto in "10.8. Reading Datafiles Within a Package" di Python Cookbook, Terza Edizione di David Beazley e Brian K. Jones che danno le risposte.

Lo porterò qui:

Supponiamo di avere un pacchetto con file organizzati come segue:

mypackage/
    __init__.py
    somedata.dat
    spam.py

Supponiamo ora che il file spam.py voglia leggere il contenuto del file somedata.dat. Per farlo, usa il codice seguente:

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

I dati variabili risultanti saranno una stringa di byte contenente i contenuti grezzi del file.

Il primo argomento di get_data () è una stringa contenente il nome del pacchetto. Puoi fornirlo direttamente o utilizzare una variabile speciale, come__package__ . Il secondo argomento è il nome relativo del file all'interno del pacchetto. Se necessario, è possibile navigare in directory diverse utilizzando le convenzioni standard per i nomi di file Unix purché la directory finale si trovi ancora all'interno del pacchetto.

In questo modo, il pacchetto può essere installato come directory, .zip o .egg.


Mi piace che tu abbia fatto riferimento al ricettario!
A. Hendry

0

La risposta accettata dovrebbe essere quella di utilizzare importlib.resources. pkgutil.get_datarichiede anche che l'argomento non packagesia un pacchetto dello spazio dei nomi ( vedere la documentazione di pkgutil ). Quindi, la directory contenente la risorsa deve avere un __init__.pyfile, in modo che abbia le stesse identiche limitazioni di importlib.resources. Se il problema generale di pkg_resourcesnon è un problema, anche questa è un'alternativa accettabile.



-3

supponendo che tu stia usando un file uovo; non estratto:

L'ho "risolto" in un progetto recente, utilizzando uno script di postinstallazione, che estrae i miei modelli dall'uovo (file zip) nella directory corretta nel filesystem. È stata la soluzione più rapida e affidabile che ho trovato, dal momento che lavorare con __path__[0]può andare storto a volte (non ricordo il nome, ma ho visto almeno una libreria, che ha aggiunto qualcosa davanti a quella lista!).

Inoltre, i file uovo vengono solitamente estratti al volo in una posizione temporanea chiamata "cache delle uova". È possibile modificare tale posizione utilizzando una variabile di ambiente, prima di avviare lo script o anche successivamente, ad es.

os.environ['PYTHON_EGG_CACHE'] = path

Tuttavia ci sono pkg_resources che potrebbero fare il lavoro correttamente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.