Leggere risorse incluse nei pacchetti Python con <code>importlib.resources</code>

by theArchitect
SHARE
Leggere risorse incluse nei pacchetti Python con <code>importlib.resources</code>
© Guida-HTML5.it

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.resources ai 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-8 evita 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.resources nella libreria standard.
  • Python Packaging User Guide: gestione dei file di dati inclusi nei pacchetti.
  • Documentazione JSON: modulo standard json per serializzare e deserializzare dati.
  • PEP 273 e PEP 302: approfondimenti sul caricamento dei moduli e sulle risorse associate ai package.

SHARE