Aiuto, la finestrella nera!

Una richiesta tipica del principiante è “come faccio a nascondere la finestrella nera?”. In realtà è una vera ossessione: la “finestrella nera” è in assoluto la cosa più odiata dal principiante. Anche se ho già affrontato l’argomento in alcuni punti della mia guida, provo qui a sviluppare e chiarire meglio il discorso.
In primo luogo, diciamo che l’odio per la “finestrella nera” è parente stretto dell’amore per le interfacce grafiche, e la domanda “come faccio a nascondere la finestrella” viene subito prima o subito dopo quella, altrettanto celebre, “e adesso dove clicco”. Quindi la risposta è identica: Python è un programma a riga di comando, in linea di principio. Non “si clicca” da nessuna parte, e la “finestrella nera” è appunto l’indispensabile interfaccia di Python. Detto questo, approfondiamo il discorso.

La console di Windows.

La famigerata “finestrella nera” non è altro che il prompt dei comandi di Windows. Più precisamente, è la finestra del Windows Console Host (conhost.exe), dentro la quale viene tipicamente eseguito il prompt dei comandi (cmd.exe), ma non necessariamente: la console potrebbe ospitare una shell alternativa come PowerShell, e così via. Potete aprire una shell in molti modi: per esempio con la combinazione di tasti win+R e poi scrivendo cmd<invio>.
In Windows, un “programma” (un processo) può essere “GUI” (Win32 host) o “a riga di comando” (console host): un processo che richiede la riga di comando verrà appunto eseguito attraverso la shell (cmd.exe) e quindi provocherà l’apertura della console (conhost.exe). Un processo a interfaccia grafica (GUI), viceversa, non ha bisogno della shell per funzionare. Questa distinzione è una caratteristica del processo stesso, non del modo in cui viene chiamato. Per esempio, la calcolatrice è un programma GUI: uno di quelli che potete avviare normalmente facendo doppio clic sulla sua icona, per capirci. Però provate invece ad aprire una shell e avviare la calcolatrice invocando direttamente il suo eseguibile: 
> calc
La calcolatrice si avvia e (attenzione!) la shell non resta bloccata (l’esecuzione è asincrona) perché il processo (GUI) della calcolatrice è diverso, separato, da quello della shell che abbiamo usato per lanciarla. Una volta che la calcolatrice è avviata, possiamo anche chiudere la finestra della shell: la calcolatrice resterà in vita normalmente. Questo è il modo in cui funzionano i processi GUI. Potete provare con altri programmi tipici di questo genere: notepad.exe, charmap.exe, mspaint.exe e così via.

Viceversa, netstat.exe è un tipico programma a riga di comando. Se cercate il suo eseguibile (in C:\windows\System32) e ci fate doppio clic sopra, vedrete che si apre la finestra della console mentre il programma viene eseguito (dovrebbe metterci qualche secondo, ammesso che abbiate almeno una scheda di rete sulla vostra macchina!). Al termine dell’esecuzione, la “finestrella nera” scompare da sola… semplicemente perché il processo è terminato, e la shell era la sua interfaccia con l’utente. Se provate a invocare netstat.exe da una shell già aperta, con
> netstat
vedrete che la shell resta bloccata (esecuzione sincrona) per il tempo di esecuzione del programma, appunto perché netstat.exe usa la shell da cui è chiamato. Se provate a passare attraverso il comando START, che avvia il processo richiesto “separandolo” dalla shell,
> START netstat
vedrete che si apre una seconda shell per l’esecuzione di netstat.exe e la shell originaria non resta bloccata (esecuzione asincrona). Questo è, in generale, come funzionano i processi a riga di comando.

C’è Python e Pythonw…

Nella distribuzione per Windows, l’eseguibile dell’interprete Python è presente in due versioni: python.exe è un programma a riga di comando, pythonw.exe… non lo è. Nel senso: è un programma Win32 host, anche se poi naturalmente non ha nessuna GUI da mostrare all’utente.
Eseguire uno script con pythonw.exe in genere è problematico, a meno che uno non sappia esattamente quello che sta facendo. Per provare la differenza tra i due interpreti, potete creare uno script (supponiamo di chiamarlo C:\test\test.py):
# file: C:\test\test.py
import winsound
print('qualcosa nello standard output')
# 1/0   # eventualmente, per testare un'eccezione non gestita
winsound.Beep(4000, 2000)
Questo script emette un suono di due secondi, per testimoniare che in effetti sta girando anche quando non siamo in grado di vederlo. Potete invocarlo dalla shell in diversi modi per vedere la differenza:
C:\test> python test.py
qualcosa nello standard output
C:\test> pythonw test.py
C:\test> START python test.py
C:\test> START pythonw test.py
L’interprete python.exe esegue lo script nella shell, bloccandola fino al termine dell’esecuzione. D’altra parte, pythonw.exe non blocca la shell ed esegue lo script “distaccato”.

Nota bene numero uno: questa idea di fornire due interpreti in ambiente Windows non è venuta solo agli sviluppatori Python, beninteso. Esistono wperl.exe, rubyw.exe, javaw.exe
Nota bene numero due: non confondete queste due versioni dell’eseguibile Python con il fatto che il vostro script potrebbe (o no) mostrare una GUI all’utente (con Tk, Qt, wxPython etc.). A Windows (e all’eseguibile Python, peraltro) non importa nulla di ciò che fate nel vostro script. Se il vostro script ha la GUI, si comporta allo stesso modo di tutti gli altri script: se lo eseguite con python.exe vedrete la finestrella nera (accanto alle finestre della vostra GUI, quindi), se lo eseguite con pythonw.exe, no. E questa, credo, è la risposta da un milione di dollari che il principiante cerca in modo compulsivo. Ma proseguiamo il nostro discorso.

Una differenza fondamentale tra i due interpreti è che pythonw.exe sopprime gli standard stream che quindi non sono più disponibili. Un print nello standard output non ha nessun effetto, per esempio. Ma il peggio è che anche lo standard error è fuori gioco: questo significa che un’eccezione non gestita terminerà silenziosamente il programma, perché non esiste un posto dove pubblicare lo stacktrace dell’eccezione.
In Python 2 le cose andavano anche peggio: gli standard stream con pythonw.exe erano dirottati verso descrittori non validi: questo comportava che anche un semplice print dimenticato nel codice generava un’eccezione non gestita (che a sua volta terminava silenziosamente l’esecuzione). In Python 3 pythonw.exe usa invece descrittori nulli, e questo consente almeno a print di essere una no-op. Ma ovviamente scrivere direttamente nello standard output (per esempio, sys.stdout.write()) genera pur sempre un’eccezione.

Se intendete usare pythonw.exe, come minimo bisognerebbe re-direzionare lo standard error verso un file esterno, in modo da poter ispezionare la causa di un crash. Questo può essere fatto “da dentro” Python, con qualcosa come:
if "w" in sys.executable:
    sys.stdout = open('standard_output.txt', 'a')
    sys.stderr = open('standard_error.txt', 'a')
In alternativa, potete intervenire “da fuori” al momento dell’invocazione con la shell:
C:\test> pythonw test.py 1>standard_output.txt 2>standard_error.txt
Beninteso, usare pythonw.exe può portare comunque a problemi differenti: per esempio, se lo script resta “in stallo” senza terminare in modo appropriato (per esempio, intrappolato in un ciclo while senza uscita), il processo resterà in vita senza che voi possiate interromperlo (e perfino accorgervene, in effetti).

Usare le associazioni dei file.

Usare pythonw.exe è differente se lo invochiamo attraverso una chiamata a ShellExecuteEx, ovvero quando lasciamo che sia Windows a trovare il programma giusto da eseguire, in base alle associazioni dei file. Possiamo verificarlo rinominando il nostro script test.pyw, in modo che Windows sappia che vogliamo che sia eseguito da pythonw.exe. A questo punto, dalla shell:
C:\test> test.pyw
lascerà a Windows il compito di trovare l’eseguibile adatto con cui far girare lo script, e ShellExecuteEx lo indirizzerà appunto verso pythonw.exe. Notate però che questa volta fare
C:\test> test.pyw 1>standard_output.txt 2>standard_error.txt
per re-indirizzare gli standard streams non funziona perché ShellExecuteEx tiene in considerazione gli handle passati dalla shell solo quando avvia un programma a riga di comando, ma invece non li considera per i programmi non-GUI. Per esempio,
C:\test> test.py 1>standard_output.txt 2>standard_error.txt
qui il re-indirizzamento funziona correttamente.
In definitiva, quando lanciate direttamente lo script *.pyw con le associazioni dei file, l’unica possibilità è quindi re-indirizzare “da dentro” lo script Python, come abbiamo visto sopra. Oppure, naturalmente, non usare affatto le associazioni dei file e invocare esplicitamente l’eseguibile. Le associazioni dei file, naturalmente, sono usate anche quando fate doppio clic per avviare lo script:
  • doppio-clic su test.py usa python.exe e quindi apre una shell che resta visibile fino al termine dell’esecuzione dello script;
  • rinominare il file test.pyw lo associa all’interprete pythonw.exe: a questo punto un doppio clic su test.pyw fa partire lo script senza nessuna “finestrella nera” visibile, ma anche senza nessuna possibilità di re-indirizzare “da fuori” gli standard streams.

Usare un collegamento.

Uno shortcut (collegamento) permette di eseguire un programma con qualche personalizzazione aggiuntiva, ma non è la stessa cosa di avviarlo dalla shell. Potete inserire nella “destinazione” del collegamento tutti gli argomenti che l’eseguibile accetta, ma non potete metterci dei comandi della shell: per esempio, vale scriverci dentro pythonw -m timeit "list(range(1000))", perché questi sono argomenti validi dell’eseguibile python(w).exe; ma d’altra parte non vale pythonw -m timeit "list(range(1000))" > res.txt se volete leggere il risultato in un file. Quindi, in generale, niente re-direzione “da fuori” degli standard stream, ma solo eventualmente “da dentro” il codice Python.
Un trucco potrebbe essere quello di usare il collegamento per invocare l’eseguibile cmd.exe, invece di python(w).exe, e usare quindi l’opzione /c per eseguire un comando della shell. Nel nostro esempio, sarebbe cmd /c pythonw -m timeit "list(range(1000))" > res.txt. Tuttavia questo (indovinate!) apre la famigerata finestrella nera, perché adesso il collegamento punta a cmd.exe e non più a pythonw.exe.
Infine, si intende che anche nei collegamenti potete usare le associazioni dei file come sopra: invece di mettere nella destinazione pythonw test.pyw, potete metterci anche solo test.pyw e lasciare che sia ShellExecuteEx a trovare l’eseguibile giusto in base all’estensione del file.

Usare un file batch.

Usare un file *.bat per lanciare uno script Python è abbastanza comune. In un file batch si possono scrivere più cose di quelle che stanno nella “destinazione” di un collegamento: può servire per preparare l’esecuzione dello script Python impostando variabili d’ambiente, creando risorse di vario tipo etc. Vale la pena di notare che quasi tutto si può fare anche dall’interno del codice Python: quindi ha senso usare un file batch solo per le configurazioni specifiche della singola macchina su cui intendiamo operare (o al massimo, della distribuzione Windows del nostro programma).

Dal punto di vista che ci interessa qui, il problema dei batch è che ovviamente… aprono la finestrella nera. Potete verificarlo facendo doppio clic su un semplice file test.bat che lancia lo script Python che abbiamo già visto. Basta una riga:
REM file C:\test\test.bat
REM qui le operazioni di preparazione che servono...
pythonw test.py
Anche se lo script è invocato con l’interprete pythonw.exe, non possiamo evitare che la shell si apra per eseguire il file batch che lo invoca. Possiamo mitigare questo problema, passando attraverso START:
REM file C:\test\test.bat
REM qui le operazioni di preparazione che servono...
START pythonw test.py
Questo apre il processo “in una nuova finestra”… che non esiste, trattandosi di pythonw.exe: siccome l’esecuzione è asincrona, la console usata dal file batch termina immediatamente dopo. Su un computer moderatamente performante, e se le operazioni del file batch non durano a lungo prima di avviare Python, l’effetto della finestrella che si apre e subito si chiude non dovrebbe neppure notarsi.

Usare uno script VB (o PowerShell).

Dal momento che un file batch non può fare a meno di aprire una console, se davvero abbiamo bisogno di “preparare” l’esecuzione del nostro script Python, e se non ci accontentiamo del trucco di passare attraverso START, allora non resta che utilizzare un altro linguaggio di scripting per queste operazioni preliminari, che non sia quello di cmd.exe e dei file batch. Ovviamente dobbiamo cercare qualcosa che siamo sicuri di trovare sempre in ambiente Windows… e questo restringe il campo a due opzioni: VBScript e PowerShell (che però nel nostro caso non è utile, come vedremo).

La classica combo VBScript+COM fa tanto… anni '90, ma non è il caso di formalizzarci. La cosa interessante per noi adesso è che il suo eseguibile wscript.exe (il Windows Script Host) è un processo non-GUI, quindi uno script VB non apre la famigerata finestrella nera. Possiamo quindi semplicemente creare un file C:\test\test.vbs e scriverci dentro qualcosa del genere:
' file C:\test\test.vbs
' qui le operazioni di preparazione che servono...
Dim Shell
Set Shell = WScript.CreateObject("WScript.Shell")
Shell.CurrentDirectory = "D:\test"  'eventualmente
Shell.Run "pythonw test.pyw"
Notate che il metodo Run dell’oggetto COM WScript.Shell non prevede un modo per passare la directory corrente voluta: quindi, se volete passare qualcosa di diverso da quella corrente dello script, occorre impostarla prima… ma se il vostro script non usa la directory corrente, o si accontenta di quella dello script VB, o la imposta per conto suo, potete anche ignorare la cosa.
Questo script invoca pythonw.exe, che come sappiamo non apre la console. Ma a dire il vero il metodo Run prevede anche l’opzione per lanciare un processo non-GUI nascondendo la finestra della shell. Avremmo potuto scrivere anche
Shell.Run "python test.py", 0
dove lo 0 impone di nascondere la shell, perfino se adesso stiamo invocando python.exe. Prima di lanciare l’eseguibile di Python con il nostro script, dovremmo eseguire le operazioni di preparazione che ci servono (altrimenti, a che scopo passare per uno script preliminare?).
Se però non vogliamo lavorare troppo in VB e preferiamo preparare l’ambiente a colpi di comandi della shell, nessun problema: possiamo usare VB anche solo per lanciare un file batch, che a sua volta lancerà l’eseguibile Python. Forse è un po’ convoluto, ma a volte è davvero più comodo così. Con il batch file che abbiamo già preparato, possiamo quindi scrivere il nostro script VB in questo modo:
Dim Shell
Set Shell = WScript.CreateObject("WScript.Shell")
Shell.Run "cmd /c test.bat", 0
Certo, il file batch di per sé vorrebbe aprire la console… ma lo script VB glielo impedisce!

Se VB vi sembra un po’ troppo demodè, l’alternativa giovane e cool, naturalmente, è PowerShell. Il problema è che, a differenza di wscript.exe, PowerShell è anche lui un processo “a console”, e di conseguenza uno script PowerShell non può fare a meno di aprire la finestrella nera. La tecnica di mitigazione qui consiste nell’invocare l’opzione -WindowStyle Hidden dell’eseguibile: per esempio, potete creare un collegamento e scriverci dentro qualcosa come powershell test.ps2 -WindowStyle Hidden. Anche così però la console deve comunque aprirsi almeno un istante all’inizio, prima che l’eseguibile abbia tempo di nasconderla.
Detto questo, se vi piace PowerShell e avete del lavoro “pesante” da fare prima di avviare Python, uno script PowerShell è senz’altro un’alternativa migliore ai vecchi batch file.

Usare Subprocess.

Può capitare di usare Subprocess per avviare, dall’interno dello script Python, un altro processo esterno. Talvolta potrebbe essere addirittura un secondo processo Python. Ora, l’API di Subprocess è molto ricca e permette di avere un controllo molto fine sulla modalità di esecuzione del processo, gli standard streams, l’exit code, etc.
Per quanto riguarda l’aspetto che ci interessa qui, valgono le stesse regole viste finora: se usate Subprocess per avviare un processo “console”, si aprirà la finestra della console. Questo è vero, prevedibilmente, se usate l’opzione shell=True di subprocess.Popen (o della scorciatoia subprocess.run) per eseguire comandi della shell; oppure, in modo equivalente, se usate Subprocess per eseguire cmd.exe (per esempio, subprocess.run("cmd /c ....").
Attenzione, comunque: se avete avviato lo script con pythonw.exe e dal suo interno usate Subprocess per avviare un file batch o comunque un processo a console nella stessa console, per esempio subprocess.run("netstat"), ovviamente anche il processo secondario lavorerà nella console invisibile di pythonw.exe, e questo risolve il problema alla radice. A meno che, ovviamente, il processo secondario (file batch o altro) a sua volta non decida di aprire una console per conto suo…
Se invece usate START per avviare il processo in modo “distaccato” e quindi asincrono, per esempio subprocess.run("START netstat", shell=True), si aprirà una nuova console visibile anche se il processo principale è pythonw.exe.

Riassumendo…

Python, lo ripetiamo ancora una volta, è un processo a console ed è logico e naturale che operi all’interno della shell del sistema operativo, a sua volta ospitata nella finestra della console. La console deve restare visibile, perché è il luogo dove avviene tutto l’I/O del processo Python.

Se (e solo se) sapete molto bene quello che state facendo, e siete pronti a gestire da soli l’I/O in modo alternativo a quello naturale proposto da Python, allora potete usare pythonw.exe per eseguire il vostro script Python: questo è un eseguibile Win32, senza console.
La strada più comoda per usare pythonw.exe è lasciar fare il lavoro a ShellExecuteEx rinominando lo script con l’estensione associata *.pyw. A questo punto, il doppio clic sull’icona dello script (azione cara al principiante!) lo avvia “senza la finestrella nera”. Questo è tutto ciò che occorre sapere dal lato Python del problema.

Se però avete bisogno anche di eseguire batch file o altri programmi di vario tipo (prima di avviare Python, o dal suo interno con Subprocess), ovviamente Python non è più responsabile e dovete regolarvi con il sistema operativo. Se il programma è Win32, non c’è la console (e ricordiamo in particolare che uno script VB è un programma Win32, al contrario di uno script batch o PowerShell). Viceversa, se il programma è a console, opera nella stessa console da cui viene chiamato, ovvero in una nuova console se passate da START.

Commenti

Post più popolari