Installare e usare Python su Windows (parte 7/10).

Attenzione!

Queste guide non sono più aggiornate, e in alcuni punti sono proprio superate.
Il mio libro Python in Windows tratta questi temi, e molto altro, in modo molto più completo e aggiornato. 

Avviare con il doppio clic.

Dopo aver capito, almeno a grandi linee, come funzionano gli import in Python, dobbiamo tornare sull’argomento “doppio clic”. Abbiamo già detto che gli script Python si invocano dalla shell e non facendo doppio clic sull’icona del file corrispondente - e lo confermiamo qui ancora una volta.
Tuttavia, il desiderio di trattare uno script Python come di solito si “avvia un programma” è comprensibile: ripetiamo (ancora!) che questo a rigore è un problema di pacchettizzazione e distribuzione del programma finito. Ci sono strumenti che vi consentono di generare degli eseguibili veri e propri, pronti per la distribuzione, che naturalmente si possono avviare con il doppio clic. Ne accenniamo più in là. Se volete il doppio clic, allora pacchettizzate il vostro script.
E tuttavia… il desiderio resta comunque! Può capitare di distribuire un “programma” (ovvero, destinato all’utente finale: non una “libreria”, quindi) non pacchettizzato, o semplicemente di volerlo eseguire col doppio clic sul nostro computer per noi stessi, senza perdere tempo a pacchettizzarlo. In questo caso, tuttavia, potreste incontrare due famiglie di problemi: quelli legati alla directory di lavoro e alle path relative, se il vostro script deve accedere a risorse esterne come file o database; e quelli legati alle differenti versioni di Python e ai venv necessari.

Il problema con il doppio clic.

Per cominciare, create uno script e mettetelo, per esempio, in d:/test:
# d:/test/main.py

import os
print("directory corrente:", os.getcwd()) # stampa la directory corrente
try:  
    import foo  # prova a importare d:/test/foo.py
    print('"foo.py" importato correttamente')
except ImportError:
    print('"foo.py" non importato!!')
try:
    open('foo.txt').close() # prova ad aprire foo.txt
    print('"foo.txt" aperto correttamente')
except IOError:
    print('"foo.txt" non aperto!!')
input('Premi "invio" per terminare') # aspetta senza uscire subito
Poi mettete nella stessa directory un altro script, che può anche essere un semplice file vuoto, d:/test/foo.py. Infine, create anche un file di testo vuoto e chiamatelo d:/test/foo.txt.
Anche se non avete ancora cominciato a studiare Python, il significato del codice dovrebbe essere chiaro: proviamo a importare un modulo foo.py, e poi ad accedere a una risorsa esterna (in questo caso un semplice file di testo). Notate che identifichiamo la risorsa usando una path relativa foo.txt (ovvero, ./foo.txt) invece che la path assoluta d:/test/foo.txt.
Adesso facciamo qualche esperimento. Per prima cosa aprite una shell, e portatevi in d:/test
> cd d:/test
D:\test > py main.py
directory corrente: d:\test
"foo.py" importato correttamente
"foo.txt" aperto correttamente
Premi "invio" per terminare
D:\test > 
Come vedete, la directory corrente della shell al momento di invocare lo script viene passata a Python. Non ci sono problemi per l’import di foo.py, perché come sappiamo Python finisce per trovare il modulo cercando nella directory . (che è relativa alla posizione dell’entry-point main.py che abbiamo invocato). Non ci sono problemi neppure per l’apertura di foo.txt, che viene trovato alla sua path (./foo.txt) relativa alla directory corrente d:/test.
Adesso invece portiamoci in una directory diversa da d:/test e ripetiamo l’esperimento:
> :: assicuriamoci di NON essere in d:/test...
> cd d:/
D:\ > py d:/test/main.py
directory corrente: d:\
"foo.py" importato correttamente
"foo.txt" non aperto!!
Premi "invio" per terminare
D:\ >
In questo caso l’import va ancora a buon fine come prima, ma l’apertura del file è invece compromessa. Infatti adesso la directory corrente è d:/, ma non esiste nessun file d:/foo.txt! In effetti, occorre sempre ricordarsi che gli import sono relativi alla posizione dello script eseguito, ma le path delle risorse esterne sono relative alla directory corrente passata a Python al momento dell’invocazione.
Se adesso proviamo ad avviare lo script con il doppio clic, scopriamo che la directory corrente impostata da Windows è quella del file stesso, ovvero d:/test: questo garantisce la corretta apertura del file.
Infine, possiamo verificare che anche il doppio clic su un collegamento (soft link) allo script funziona come cliccare sul file originale: ovvero, la directory corrente resta quella del file originale, non quella del link. Il collegamento però offre una possibilità in più: se fate clic con il pulsante destro, scegliete “Proprietà” e poi la scheda “Collegamento”, nella casella “Da:” potete impostare manualmente la directory corrente da passare.

Il problema con i batch file.

Talvota capita di voler invocare uno script Python attraverso un batch file (un file con estensione .bat). In questo caso però sappiate che la directory corrente passata a Python sarà quella del file batch, non quella del file dello script.
Potete verificarlo facilmente creando un file batch che contiene semplicemente questo:
REM test.bat
py d:/test/main.py
Se adesso mettete questo file in una directory qualsiasi (per esempio il desktop) e ci fate doppio clic sopra, vedrete che l’apertura del file fallisce, perché la directory corrente non è quella giusta.
Potete naturalmente impostare la directory corrente prima di avviare Python:
REM test.bat
d:
CD d:/test
py main.py

Impostare la directory corrente con Python.

In generale, lo script Python invocato nel modo “giusto” dovrebbe passare a Python la directory corrente del file dello script. Tuttavia, non potete aspettarvi che questo sia sempre vero (e anzi, talvolta è perfino preferibile che non sia così). Di conseguenza, ogni volta che volete accedere a una risorsa esterna (un file, un database…) localizzata con una path relativa, non potete essere sicuri che Python risolverà la path nel modo voluto.
Una soluzione sarebbe identificare le risorse esterne solo per mezzo di path assolute, ma chiaramente questo non va bene, perché sacrifica completamente la portabilità del vostro codice.
È però possibile impostare la directory corrente anche “da dentro” lo script Python. Modificate main.py in modo che all’inzio abbia queste righe:
# d:/test/main.py

import os, os.path
os.chdir(os.path.abspath(os.path.dirname(__file__)))
# etc. etc.
Qui, os.chdir vuol dire “imposta la directory corrente”; os.path.abspath significa “la path assoluta di…”; os.path.dirname è “la directory di…”; la variabile __file__ conserva il nome del file dello script. Se adesso riprovate tutti gli esperimenti fatti fin qui, noterete che la directory corrente resta sempre quella del file, anche se cercate di passare qualcos’altro dalla shell, o da un file batch, o con le impostazioni di un collegamento.
Se volete usare questa tecnica, vi conviene impostare la directory corrente molto presto nel vostro script: sicuramente, prima di accedere a qualsiasi risorsa esterna.

Se lo script ha bisogno di un venv.

Un altro ordine di problemi capita quando avete due o più versioni di Python installate, o magari quando il vostro script deve essere eseguito in un venv. In questi casi, naturalmente fare doppio clic lascia a Windows la scelta dell’eseguibile Python da invocare, e non sempre le cose andranno lisce.
In precedenza avevamo creato un venv di Python 3 in d:/venvs/test, e ci avevamo installato dentro il pacchetto Arrow. Se avete conservato quel venv (e in caso contrario, basta ricrerlo), potete verificare subito il problema. Modificate il nostro script main.py aggiungendo questo alla fine:
# d:/test/main.py

# ...
try:
    import arrow
    print('"Arrow" importato correttamente')
except ImportError:
    print('"Arrow" non importato!!')
input('Premi "invio" per terminare') # aspetta senza uscire subito
Se adesso eseguite lo script con il doppio clic, vedrete che Arrow non viene importato. Il motivo è semplice: Windows esegue lo script con il Python “di sistema”, che non ha Arrow installato.
Per fare in modo che Windows selezioni l’interprete Python corretto (ovvero quello del venv) quando fate doppio clic sullo script, avete diverse opzioni.
In primo luogo, potete senz’altro attivare il venv da una shell prima di fare doppio clic. Ma si capisce che questo non avrebbe molto senso, visto che state appunto cercando di evitare di usare la shell.
In secondo luogo, potete impostare la shebang dello script indirizzandola verso l’interprete corretto:
#! d:/venvs/test/scripts/python.exe

# etc etc...
Questo funziona, ma non è una buona idea: legate il vostro codice a dettagli della configurazione specifica del vostro computer, e sacrificate la portabilità.
Terzo, potete usare un collegamento. Create un collegamento al vostro script main.py, apritene le “Proprietà” e nella scheda “Collegamento”, casella “Destinazione” scrivete:
d:/venvs/test/scripts/python.exe d:/test/main.py
Accertatevi anche che la casella “Da:” contenga la path della directory corrente che volete passare a Python. Adesso, se fate doppio clic sul collegamento, Windows eseguirà lo script con l’interprete giusto. Questa soluzione è buona: non dovete toccare il vostro codice, e i dettagli di configurazione restano specificati in un collegamento che potete modificare separatamente.
Infine, quarto, potete usare un file batch in modo del tutto analogo:
REM test.bat
d:
CD d:/test
d:/venvs/test/scripts/python.exe main.py
Se preferite, potete usare il file batch per attivare il venv:
REM test.bat
REM ...
CALL d:/venvs/test/scripts/activate.bat
py main.py
O anche, siccome l’attivazione di un venv è in sostanza una manipolazione della path di sistema:
REM test.bat
REM ...
SET path=d:/venvs/test/scripts;%path%
py main.py
Usare un file batch è un’altra buona soluzione, che vi consente di separare dal codice Python i dettagli della vostra configurazione specifica. Il vantaggio di un file batch rispetto a un semplice collegamento “preparato”, è che vi permette all’occorrenza di fare molte cose in più prima di avviare lo script Python. Per esempio potete impostare delle variabili d’ambiente, creare file e directory di lavoro, e in generale preparare l’esecuzione dello script senza inquinare il codice Python con dettagli specifici per la vostra configurazione.

Che cosa fare con l’output dello script.

Talvolta il vostro script produce un output che desiderate vedere nella shell: per esempio, in seguito a un print. Se invocate lo script dalla shell, questa resta aperta durante l’esecuzione. Ma se fate doppio clic, tipicamente la shell (la “finestrella nera”, per capirci) si apre, esegue lo script, mostra l’output e poi si chiude: tutto questo avviene troppo rapidamente per permettervi di capirci qualcosa. Per esempio, se avete uno script così…
# d:/test/foo.py

print(2+2)
…e ci fate doppio clic sopra, non farete mai in tempo a vedere che per Python, effettivamente, 2+2 fa 4.
La soluzione standard è quella che abbiamo già adottato negli esempi precedenti: fare in modo che lo script termini con una richiesta di input per l’utente. In questo modo lo script si blocca (e la shell non si chiude!) finché l’utente non ha immesso l’input premendo invio. La funzione da usare è raw_input in Python 2, e input in Python 3:
# d:/test/foo.py

print(2+2)
input('Premi "invio" per terminare!')
# oppure raw_input('...') in Python 2

Che cosa fare in caso di errore.

Può capitare che lo script incontri un’eccezione non gestita (ovvero, non compresa in un blocco try/except), il che di solito corrisponde a una situazione di errore imprevisto. In questo caso, Python si arresta inaspettatamente dopo aver pubblicato sullo stream dello “standard error” il traceback dell’eccezione incontrata: naturalmente, lo “standard error” è diretto verso la shell, di solito. Questo significa che avete lo stesso problema di prima. Se avviate lo script dalla shell, tutto va bene: Python si arresta, ma la shell resta aperta e voi potete esaminare il traceback dell’errore incontrato e capire il problema. Ma se fate doppio clic, la shell si chiude immediatamente e voi restate senza informazioni.
Questa situazione non può essere risolta con un input finale, perché l’eccezione capiterà appunto prima che Python arrivi alla fine dello script:
# d:/test/foo.py

print(2+2)
try:
    print(1/0)  # eccezione gestita da un try/except
except ZeroDivisionError:
    print('Tutto bene... vado avanti!')
print(1/0) # OPS! eccezione non gestita!
input('Premi "invio" per terminare!')
Qui Python pubblicherà il risultato della prima istruzione (4) nello “standard output” (la shell). Poi incontrerà una eccezione gestita in un blocco try/except, scriverà un messaggio nello “standard output” e andrà avanti. Poi però incontrerà un’eccezione non gestita, e a questo punto pubblicherà lo stacktrace nello “standard error” (sempre la shell). Infine terminerà l’esecuzione dello script prematuramente, prima di arrivare all’input finale.
Certo, la verità è che, almeno in un software destinato all’utente finale, tutte le eccezioni dovrebbero essere previste e gestite. Un’eccezione non gestita è un baco. Ma appunto, anche i bachi succedono, e ci serve lo stacktrace dell’eccezione per poterli correggere. In fase di sviluppo di solito questo non è un problema perché voi avviate sempre lo script dalla shell. Ma un baco può capitare anche dopo, quando ormai considerate il vostro programma “finito” e volete avviarlo comodamente con il doppio clic.
La cosa peggiore è che un errore di questo tipo potrebbe capitare non solo all’interno del codice Python vero e proprio, ma anche a causa di una gestione sbagliata dell’ambiente di esecuzione. Per esempio, se facendo doppio clic avviate lo script con l’interprete sbagliato, Python potrebbe incontrare immediatamente degli ImportError (se magari non siete nel venv giusto), o addirittura dei SyntaxError (se state eseguendo lo script con una versione sbagliata di Python). Questo è il famigerato caso del “flash della finestrella nera”: fate doppio clic, e tutto ciò che vedete è la finestra della shell che si apre per un istante e poi subito si chiude. La frustrazione cresce perché talvolta invece, eseguendo lo script dalla shell, tutto funziona (perché la shell è nel venv giusto, o seleziona la versione giusta di Python).
Ci sono diverse possibilità per risolvere questi problemi. Una idea valida, anche se un po’ rozza, è re-direzionare lo stream dello “standard error” verso un file di log, invece che nella shell. In caso di errore, in seguito potete aprire il file e ispezionare lo stacktrace:
# d:/test/foo.py

import sys
sys.stderr = open('stderr.txt', 'a')
print(1/0)
Questo non è del tutto a prova di bomba, ma basta e avanza per i casi più comuni. In particolare, vi salva dalle eccezioni non gestite (come lo ZeroDivisionError qui sopra) e anche dagli ImportError iniziali dovuti all’interprete sbagliato. Naturalmente dovreste re-direzionare lo “standard error” molto presto nel vostro codice, e senz’altro prima del primo import che potrebbe fallire. Tenete conto però che questo trucco non può fare nulla contro eventuali SyntaxError che vengono rilevati da Python in fase di compilazione, e quindi prima che il vostro codice venga effettivamente eseguito. Possono esserci situazioni particolari in cui Python si interrompe prima di essere riuscito a completare la scrittura dello stracktrace nel file, e situazioni in cui il file stesso risulta mancante, corrotto, etc. Senza contare, naturalmente, i rari casi in cui lo stesso interprete Python (e non solo il vostro codice, quindi) può andare in crash.
Una possibilità analoga, ma più raffinata, è ricorrere a sys.excepthook: questa funzione contiene del codice da eseguire come ultima istanza, esclusivamente in caso di eccezione non gestita. Potete sovrascriverla e fare quello che desiderate in questa fase: in questo esempio noi scriviamo lo stacktrace nella shell e, invece di uscire immediatamente, blocchiamo lo script con il trucco che già conosciamo:
# d:/test/foo.py

import sys, traceback
def excepthook(exc_type, exc_value, exc_traceback):
    print("Ecco un'eccezione non gestita!!")
    traceback.print_exception(exc_type, exc_value, exc_traceback)
    input('Premi "invio" per terminare.')

sys.excepthook = excepthook

print(1/0)
Notate che, per la stessa ragione di prima, questo meccanismo non vi difende dai SyntaxError né dagli “hard crash” dell’interprete Python, e comunque andrebbe inserito molto in alto nel vostro codice. Usare sys.excepthook è comunque, in generale, un’idea migliore del semplice re-direzionamento dello “standard error”. Nella vostra funzione potete usare un sistema di logging per conservare lo stacktrace dell’eccezione, emettere un messaggio di avviso per l’utente, prendervi il tempo chiudere le risorse rimaste aperte (connessioni al database…) ed eseguire altre operazioni di cleanup prima di terminare il programma. Fate solo attenzione, naturalmente, che queste operazioni non inneschino a loro volta altre eccezioni.
Infine, ricordate che tutti questi rimedi valgono solo a partire dal momento in cui il controllo passa all’interprete Python. Se fate qualche errore prima, per esempio nel file batch che avvia lo script, oppure se sbagliate a scrivere la path dell’interprete nelle impostazioni del collegamento… questi sono errori che non riguardano Python, e che dovete correggere per conto vostro.

Commenti

Post più popolari