Introduzione
Un pacchetto Python non contiene necessariamente soltanto file .py. In molti progetti è utile distribuire insieme al codice anche file JSON, CSV, modelli, template o altri dati necessari all’applicazione. Un esempio tipico è un pacchetto che contiene un catalogo di prodotti, un file di traduzione o una configurazione predefinita.
Un errore comune consiste nel costruire manualmente il percorso del file partendo da __file__. Questa tecnica può funzionare durante lo sviluppo locale, ma diventa fragile quando il pacchetto viene installato, compresso o caricato da un archivio. La soluzione moderna e portabile è il modulo standard importlib.resources, che permette di accedere alle risorse associate a un pacchetto senza dipendere dalla struttura fisica del filesystem.
In questo tutorial costruiremo un piccolo pacchetto chiamato catalogo. Il pacchetto caricherà un file JSON contenente alcuni prodotti e offrirà funzioni per cercarli e visualizzarli.
Codice completo
Organizziamo il progetto in questo modo:
negozio_demo/
├── catalogo/
│ ├── __init__.py
│ ├── servizi.py
│ └── dati/
│ ├── __init__.py
│ └── prodotti.json
└── app.py Il file prodotti.json contiene i dati distribuiti insieme al pacchetto:
[
{
"id": 1,
"nome": "Tastiera meccanica",
"categoria": "accessori",
"prezzo": 89.90
},
{
"id": 2,
"nome": "Monitor 27 pollici",
"categoria": "schermi",
"prezzo": 249.50
},
{
"id": 3,
"nome": "Mouse ergonomico",
"categoria": "accessori",
"prezzo": 39.99
}
] Il file catalogo/servizi.py contiene il codice che legge la risorsa:
import json
from importlib.resources import files
def carica_prodotti():
"""Legge prodotti.json incluso nel pacchetto catalogo."""
# files() restituisce un oggetto che rappresenta
# la cartella delle risorse del pacchetto indicato.
percorso = files("catalogo").joinpath("dati", "prodotti.json")
# open() funziona anche se la risorsa non è un normale
# file presente direttamente sul disco.
with percorso.open("r", encoding="utf-8") as file:
return json.load(file)
def cerca_per_categoria(categoria):
"""Restituisce i prodotti appartenenti a una categoria."""
prodotti = carica_prodotti()
return [
prodotto
for prodotto in prodotti
if prodotto["categoria"].lower() == categoria.lower()
]
def prezzo_medio():
"""Calcola il prezzo medio dei prodotti."""
prodotti = carica_prodotti()
if not prodotti:
return 0.0
totale = sum(prodotto["prezzo"] for prodotto in prodotti)
return totale / len(prodotti) Possiamo poi usare il pacchetto dall’applicazione principale:
from catalogo.servizi import cerca_per_categoria, prezzo_medio
def main():
accessori = cerca_per_categoria("accessori")
print("Prodotti della categoria accessori:")
for prodotto in accessori:
print(f"- {prodotto[´nome´]}: €{prodotto[´prezzo´]:.2f}")
print(f"nPrezzo medio: €{prezzo_medio():.2f}")
if __name__ == "__main__":
main() Avviando l’applicazione dalla directory del progetto, si ottiene un risultato simile:
Prodotti della categoria accessori:
- Tastiera meccanica: €89.90
- Mouse ergonomico: €39.99
Prezzo medio: €126.46 Spiegazione
Perché usare importlib.resources
La funzione files("catalogo") individua le risorse associate al pacchetto catalogo. Con joinpath() raggiungiamo il file desiderato senza concatenare stringhe di percorso e senza usare direttamente os.path o pathlib.Path(__file__).
Il metodo open() restituisce un file aperto in modalità testo. Specificare encoding="utf-8" è importante per gestire correttamente accenti e caratteri internazionali. Infine, json.load(file) converte il contenuto JSON in una lista di dizionari Python.
Il ruolo di dati/__init__.py
In questo esempio dati è una sottocartella dedicata alle risorse. Inserire un file __init__.py la rende un pacchetto chiaramente riconoscibile e facilita la gestione coerente delle risorse. Il codice applicativo, però, usa direttamente il pacchetto principale catalogo come punto di riferimento:
files("catalogo").joinpath("dati", "prodotti.json") Non è necessario aprire il file usando un percorso assoluto. Questo rende il codice più adatto a installazioni diverse e a sistemi operativi differenti.
Usare una risorsa temporaneamente come file
Alcune librerie esterne richiedono un vero percorso sul filesystem, invece di un oggetto risorsa. In questi casi si può usare as_file():
from importlib.resources import files, as_file
risorsa = files("catalogo").joinpath("dati", "prodotti.json")
with as_file(risorsa) as percorso_temporaneo:
print(f"Percorso utilizzabile dalla libreria: {percorso_temporaneo}") Il percorso potrebbe essere temporaneo e valido soltanto all’interno del blocco with. Non bisogna quindi conservarlo per un uso successivo.
Best practice
- Preferire
importlib.resourcesai percorsi manuali: il codice resta indipendente dal modo in cui il pacchetto è installato. - Separare dati e logica: collocare JSON, template e file statici in una cartella dedicata rende il progetto più leggibile.
- Specificare sempre l’encoding: usare
utf-8evita problemi quando i file contengono caratteri accentati. - Gestire gli errori di lettura: file mancanti o JSON non validi devono produrre messaggi comprensibili.
- Evitare di modificare le risorse del pacchetto: i dati inclusi sono normalmente da considerare in sola lettura.
- Validare il contenuto: prima di usare i dati, verificare che abbiano le chiavi e i tipi attesi.
- Considerare il caching: se il file viene letto spesso, caricarlo una sola volta può migliorare le prestazioni.
Una versione più robusta può gestire gli errori principali:
import json
from importlib.resources import files
def carica_prodotti():
percorso = files("catalogo").joinpath("dati", "prodotti.json")
try:
with percorso.open("r", encoding="utf-8") as file:
dati = json.load(file)
except FileNotFoundError as errore:
raise RuntimeError("Il catalogo dei prodotti non è disponibile") from errore
except json.JSONDecodeError as errore:
raise RuntimeError("Il catalogo contiene JSON non valido") from errore
if not isinstance(dati, list):
raise ValueError("Il catalogo deve contenere una lista")
return dati Riepilogo
Le risorse non Python fanno parte integrante di molti pacchetti reali. Il modulo standard importlib.resources consente di leggerle in modo portabile e affidabile, senza legare l’applicazione a percorsi assoluti o alla struttura fisica del progetto.
La tecnica fondamentale consiste nell’usare files("nome_pacchetto"), raggiungere la risorsa con joinpath() e aprirla con open(). Quando una libreria richiede un percorso reale, as_file() offre un adattatore temporaneo. Separare correttamente codice e dati rende i pacchetti più riutilizzabili, testabili e semplici da distribuire.
Approfondisci con risorse ufficiali
- Documentazione Python: modulo
importlib.resourcesnella libreria standard. - Python Packaging User Guide: gestione dei file di dati inclusi nei pacchetti.
- Documentazione JSON: modulo standard
jsonper serializzare e deserializzare dati. - PEP 273 e PEP 302: approfondimenti sul caricamento dei moduli e sulle risorse associate ai package.
