Esercitazione 8 - Renderlo liscio

A meno che non si disponga di una connessione Internet molto veloce, si potrebbe notare che quando si preme il pulsante, l’interfaccia grafica dell’applicazione si blocca per un po”. Questo perché la richiesta web che abbiamo fatto è sincrona. Quando l’applicazione effettua la richiesta web, attende che l’API restituisca una risposta prima di continuare. Mentre aspetta, non permette all’applicazione di ridisegnare e di conseguenza l’applicazione si blocca.

Loop di eventi della GUI

Per capire perché questo accade, dobbiamo entrare nei dettagli del funzionamento di un’applicazione GUI. Le specifiche variano a seconda della piattaforma, ma i concetti di alto livello sono gli stessi, indipendentemente dalla piattaforma o dall’ambiente GUI utilizzato.

Un’applicazione GUI è, fondamentalmente, un singolo ciclo che assomiglia a:

while not app.quit_requested():
    app.process_events()
    app.redraw()

Questo ciclo è chiamato Event Loop. (Non si tratta di nomi di metodi reali, ma di un’illustrazione di ciò che avviene in «pseudo-codice»).

Quando si fa clic su un pulsante, si trascina una barra di scorrimento o si digita un tasto, si genera un «evento». Questo «evento» viene inserito in una coda e l’applicazione elaborerà la coda di eventi quando ne avrà l’opportunità. Il codice utente che viene attivato in risposta all’evento è chiamato gestore di eventi. Questi gestori di eventi vengono invocati come parte della chiamata process_events().

Una volta che un’applicazione ha elaborato tutti gli eventi disponibili, essa ridisegna() la GUI. Questa operazione tiene conto di tutti i cambiamenti che gli eventi hanno causato alla visualizzazione dell’applicazione, nonché di qualsiasi altra cosa stia accadendo nel sistema operativo: ad esempio, le finestre di un’altra applicazione possono oscurare o rivelare parte della finestra della nostra applicazione, e il ridisegno della nostra applicazione dovrà riflettere la porzione di finestra attualmente visibile.

Il dettaglio importante da notare: mentre un’applicazione sta elaborando un evento, non può ridisegnare e non può elaborare altri eventi.

Ciò significa che qualsiasi logica utente contenuta in un gestore di eventi deve essere completata rapidamente. Qualsiasi ritardo nel completamento del gestore di eventi sarà osservato dall’utente come un rallentamento (o un arresto) degli aggiornamenti della GUI. Se il ritardo è sufficientemente lungo, il sistema operativo può segnalarlo come un problema: le icone «beachball» di macOS e «spinner» di Windows indicano che l’applicazione sta impiegando troppo tempo in un gestore di eventi.

Operazioni semplici come «aggiornare un’etichetta» o «ricalcolare il totale degli input» sono facili da completare rapidamente. Tuttavia, ci sono molte operazioni che non possono essere completate rapidamente. Se si sta eseguendo un calcolo matematico complesso, o l’indicizzazione di tutti i file di un file system, o l’esecuzione di una richiesta di rete di grandi dimensioni, non è possibile «farlo rapidamente»: le operazioni sono intrinsecamente lente.

Quindi, come si eseguono operazioni di lunga durata in un’applicazione GUI?

Programmazione asincrona

Abbiamo bisogno di un modo per dire a un’applicazione, nel mezzo di un gestore di eventi di lunga durata, che va bene rilasciare temporaneamente il controllo al ciclo di eventi, a patto che si possa riprendere da dove si era interrotto. Spetta all’applicazione determinare quando questo rilascio può avvenire; ma se l’applicazione rilascia regolarmente il controllo al ciclo di eventi, possiamo avere un gestore di eventi di lunga durata e mantenere un’interfaccia utente reattiva.

È possibile farlo utilizzando la programmazione asincrona. La programmazione asincrona è un modo per descrivere un programma che consente all’interprete di eseguire più funzioni contemporaneamente, condividendo le risorse tra tutte le funzioni in esecuzione simultanea.

Le funzioni asincrone (note come co-routine) devono essere dichiarate esplicitamente come asincrone. Inoltre, devono dichiarare internamente quando esiste la possibilità di cambiare contesto a un’altra co-routine.

In Python, la programmazione asincrona è implementata utilizzando le parole chiave async e await e il modulo asyncio della libreria standard. La parola chiave async ci permette di dichiarare che una funzione è una co-routine asincrona. La parola chiave await fornisce un modo per dichiarare quando esiste l’opportunità di cambiare contesto a un’altra co-routine. Il modulo asyncio fornisce altri strumenti e primitive utili per la codifica asincrona.

Rendere l’esercitazione asincrona

Per rendere il nostro tutorial asincrono, modificare il gestore dell’evento say_hello() in modo che assomigli a questo:

async def say_hello(self, widget):
    async with httpx.AsyncClient() as client:
        response = await client.get("https://jsonplaceholder.typicode.com/posts/42")

    payload = response.json()

    self.main_window.info_dialog(
        greeting(self.name_input.value),
        payload["body"],
    )

In questo codice ci sono solo 4 modifiche rispetto alla versione precedente:

  1. Il metodo è definito come async def, invece che semplicemente def. Questo indica a Python che il metodo è una co-routine asincrona.

  2. Il client creato è un asincrono AsyncClient(), invece di un sincrono Client(). Questo indica a httpx che deve operare in modalità asincrona, anziché sincrona.

  3. Il gestore di contesto usato per creare il client è contrassegnato come async. Questo indica a Python che c’è l’opportunità di rilasciare il controllo quando il gestore di contesto viene inserito e abbandonato.

  4. La chiamata get viene effettuata con la parola chiave await. Questo indica all’applicazione che, mentre si attende la risposta dalla rete, l’applicazione può rilasciare il controllo al ciclo degli eventi.

Toga consente di utilizzare metodi regolari o co-routine asincrone come gestori; Toga gestisce tutto dietro le quinte per assicurarsi che il gestore sia invocato o atteso come richiesto.

Se si salvano queste modifiche e si esegue nuovamente l’applicazione (con briefcase dev in modalità di sviluppo, oppure aggiornando ed eseguendo nuovamente l’applicazione confezionata), non ci saranno cambiamenti evidenti nell’applicazione. Tuttavia, quando si fa clic sul pulsante per attivare la finestra di dialogo, si possono notare alcuni sottili miglioramenti:

  • Il pulsante torna a uno stato «non cliccato», anziché essere bloccato in uno stato «cliccato».

  • L’icona «beachball»/»spinner» non appare

  • Se si sposta/ridimensiona la finestra dell’applicazione mentre si attende la visualizzazione della finestra di dialogo, la finestra verrà ridisegnata.

  • Se si tenta di aprire il menu di un’applicazione, il menu viene visualizzato immediatamente.

Prossimi passi

Ora abbiamo un’applicazione che è efficiente e reattiva, anche quando è in attesa di un’API lenta. Ma come possiamo assicurarci che l’applicazione continui a funzionare mentre continuiamo a svilupparla? Come possiamo testare la nostra applicazione? Consultate Tutorial 9 per scoprirlo…