Il percorso per la verifica dei tipi di 4 milioni di righe di codice Python. Parte 2

Oggi pubblichiamo la seconda parte della traduzione riguardante come Dropbox ha organizzato il controllo dei tipi di diversi milioni di righe di codice Python.

Il percorso per la verifica dei tipi di 4 milioni di righe di codice Python. Parte 2

Leggi la prima parte

Supporto ufficiale per i tipi (PEP 484)

Abbiamo condotto i primi esperimenti significativi con mypy in Dropbox durante la Hack Week del 2014. La Hack Week è un evento che si svolge in Dropbox per una settimana. In questo periodo, i dipendenti possono lavorare su qualsiasi progetto! Alcuni dei progetti tecnologici più celebri di Dropbox sono iniziati proprio durante questi eventi. Da questo esperimento, abbiamo concluso che mypy sembra promettente, sebbene questo progetto non fosse ancora pronto per un uso su larga scala.

All'epoca circolava l'idea di standardizzare i sistemi di suggerimenti per i tipi Python. Come ho già detto, a partire da Python 3.0 era possibile utilizzare le annotazioni dei tipi per le funzioni, ma erano solo espressioni arbitrarie, senza una sintassi e una semantica definiti. Durante l'esecuzione del programma, queste annotazioni venivano per lo più semplicemente ignorate. Dopo il Hack Week, abbiamo iniziato a lavorare sulla standardizzazione della semantica. Questo lavoro ha portato alla nascita PEP 484 (questo documento è stato sviluppato in collaborazione tra Guido van Rossum, Łukasz Lange e me).

Le nostre motivazioni possono essere interpretate da due prospettive. In primo luogo, speravamo che l'intero ecosistema Python potesse adottare un approccio comune all'uso dei suggerimenti sui tipi (type hints — un termine usato in Python come equivalente delle “annotazioni di tipo”). Considerate i rischi potenziali, ciò sarebbe stato preferibile rispetto all'uso di molteplici approcci incompatibili tra loro. In secondo luogo, volevamo discutere apertamente meccanismi di annotazione dei tipi con molti membri della comunità Python. In parte, questo desiderio nasceva dal fatto che non volevamo apparire come «eretici» rispetto alle idee fondamentali del linguaggio agli occhi di un vasto pubblico di programmatori Python. Si tratta di un linguaggio tipizzato dinamicamente, noto per la sua «tipizzazione anatra». All'inizio, nella comunità, c'era una certa diffidenza verso l'idea della tipizzazione statica. Tuttavia, tale atteggiamento alla fine si è attenuato — dopo che è diventato chiaro che la tipizzazione statica non sarebbe stata resa obbligatoria (e dopo che le persone hanno capito che era davvero utile).

La sintassi adottata alla fine per i suggerimenti sui tipi era molto simile a quella supportata da mypy all'epoca. Il documento PEP 484 è stato rilasciato insieme a Python 3.5 nel 2015. Python non era più un linguaggio che supportava solo la tipizzazione dinamica. Mi piace pensare a questo evento come a una tappa significativa nella storia di Python.

Inizio della migrazione

Alla fine del 2015, in Dropbox, per lavorare su mypy, è stato creato un team di tre persone. Composto da Guido van Rossum, Greg Price e David Fisher, questo è stato l'inizio di un rapido sviluppo. Il primo ostacolo alla crescita di mypy è stata la performance. Come ho accennato in precedenza, all'inizio dello sviluppo del progetto, stavo pensando di portare l'implementazione di mypy nel linguaggio C, ma questa idea è stata temporaneamente scartata. Ci siamo trovati bloccati con l'interprete CPython, che non era sufficientemente veloce per strumenti come mypy. (Il progetto PyPy, un'implementazione alternativa di Python con compilatore JIT, non ci ha aiutato.)

Fortunatamente, siamo stati aiutati da alcuni miglioramenti algoritmici. La prima potente "accelerazione" è stata l'implementazione del controllo incrementale. L'idea di questo miglioramento era semplice: se tutte le dipendenze del modulo non erano cambiate dall'ultima esecuzione di mypy, potevamo utilizzare, durante il lavoro con le dipendenze, i dati memorizzati nella cache durante la sessione precedente. Dobbiamo solo eseguire il controllo dei tipi nei file modificati e in quelli che dipendono da essi. Mypy è andato anche oltre: se l'interfaccia esterna del modulo non era cambiata, mypy presumeva che non fosse necessario controllare di nuovo gli altri moduli che importano questo modulo.

Il controllo incrementale ci ha aiutato molto nell'annotazione di grandi volumi di codice esistente. Infatti, questo processo di solito comporta numerosi avvii iterativi di mypy, poiché le annotazioni vengono aggiunte gradualmente al codice e migliorate progressivamente. La prima esecuzione di mypy era comunque molto lenta, dato che doveva verificare molte dipendenze. Per migliorare la situazione, abbiamo implementato un meccanismo di caching remoto. Se mypy rileva che la cache locale è probabilmente obsoleta, carica l'attuale istantanea della cache per l'intera base di codice da un repository centralizzato. Quindi, esegue un controllo incrementale utilizzando questa istantanea. Questo ci ha spostati ulteriormente verso un aumento delle prestazioni di mypy.

È stato un periodo di rapida e naturale introduzione del sistema di controllo dei tipi in Dropbox. Entro la fine del 2016, avevamo circa 420.000 righe di codice Python con annotazioni di tipo. Molti utenti hanno accolto con entusiasmo il controllo dei tipi. In Dropbox, mypy è stato utilizzato da un numero crescente di squadre di sviluppatori.

Allora sembrava andare bene, ma avevamo ancora molto da fare. Iniziammo a condurre sondaggi interni periodici per identificare i punti critici del progetto e capire quali problemi affrontare per primi (questa pratica è utilizzata in azienda ancora oggi). Le due questioni più importanti, come si è rivelato, erano chiare. Prima di tutto, era necessario un maggiore copertura del codice con i tipi, in secondo luogo, era fondamentale che mypy funzionasse più velocemente. Era evidente che il nostro lavoro per velocizzare mypy e integrarlo nei progetti dell'azienda era ancora lontano dal suo completamento. Consapevoli dell'importanza di questi due compiti, ci siamo messi al lavoro per risolverli.

Più prestazioni!

I controlli incrementali hanno velocizzato mypy, ma questo strumento era ancora lontano dall'essere sufficientemente veloce. Molti controlli incrementali duravano circa un minuto. La causa di ciò erano gli import ciclici. Questo probabilmente non sorprende chi ha mai lavorato con grandi basi di codice scritte in Python. Avevamo set composti da centinaia di moduli, ognuno dei quali importava indirettamente tutti gli altri. Se qualsiasi file nel ciclo di importazioni veniva modificato, mypy doveva elaborare tutti i file coinvolti in quel ciclo e spesso anche eventuali moduli che importavano moduli da quel ciclo. Uno di questi cicli era il famigerato "nodo delle dipendenze", che ha causato molti problemi in Dropbox. Una volta questa struttura conteneva diverse centinaia di moduli, ed era importata, direttamente o indirettamente, da molti test e utilizzata anche nel codice di produzione.

Abbiamo preso in considerazione la possibilità di "disambiguare" le dipendenze cicliche, ma non avevamo le risorse per farlo. C'era troppo codice con cui non eravamo familiari. Alla fine, abbiamo optato per un approccio alternativo. Abbiamo deciso di far funzionare mypy rapidamente anche in presenza di "matasse di dipendenze". Abbiamo raggiunto questo obiettivo utilizzando un demone mypy. Il demone è un processo server che implementa due interessanti funzionalità. Innanzitutto, mantiene in memoria informazioni su tutta la base di codice. Questo significa che ad ogni avvio di mypy non è necessario caricare i dati cache riguardanti migliaia di dipendenze importate. In secondo luogo, analizza con attenzione, a livello di piccole unità strutturali, le dipendenze tra funzioni e altre entità. Ad esempio, se una funzione foo chiama la funzione bar, allora esiste una dipendenza foo da bar. Quando un file viene modificato, il demone elabora inizialmente solo il file cambiato in isolamento. Poi esamina le modifiche di quel file che sono visibili dall'esterno, come le firme delle funzioni che sono cambiate. Il demone utilizza informazioni dettagliate sulle importazioni solo per ri-verificare le funzioni che utilizzano realmente la funzione modificata. Di solito, con questo approccio, è necessario verificare solo poche funzioni.

Realizzare tutto ciò non è stato semplice, poiché l'implementazione originale di mypy era fortemente orientata al trattamento di un solo file alla volta. Abbiamo dovuto affrontare numerosi casi limite, la cui occorrenza richiedeva ripetute verifiche quando qualcosa nel codice veniva cambiato. Ad esempio, accade quando a una classe viene assegnata una nuova classe base. Dopo aver completato ciò che volevamo, siamo riusciti a ridurre il tempo di esecuzione della maggior parte dei controlli incrementali a pochi secondi. Questo ci è sembrato un grande successo.

Ancora più prestazioni!

Insieme alla caching remoto di cui ho parlato sopra, il demone mypy ha praticamente risolto gran parte dei problemi che si presentavano quando un programmatore eseguiva frequentemente il controllo dei tipi apportando modifiche a un numero limitato di file. Tuttavia, le prestazioni del sistema, nel peggiore dei casi, erano ancora lontane dall'ottimale. Un avvio pulito di mypy poteva richiedere più di 15 minuti. E questo era decisamente più di quanto avremmo gradito. Ogni settimana la situazione peggiorava, poiché i programmatori continuavano a scrivere nuovo codice e ad aggiungere annotazioni al codice esistente. I nostri utenti desideravano ancora maggiore performance e noi eravamo pronti ad andare incontro alle loro esigenze.

Abbiamo deciso di tornare a una delle idee iniziali riguardo a mypy. In particolare, alla trasformazione del codice Python in codice C. Gli esperimenti con Cython (un sistema che permette di tradurre codice scritto in Python in codice C) non ci hanno fornito alcun miglioramento visibile, quindi abbiamo deciso di riscoprire l'idea di scrivere un nostro compilatore. Poiché la base di codice di mypy (scritta in Python) conteneva già tutte le annotazioni di tipo necessarie, ci è sembrato valido tentare di utilizzare queste annotazioni per migliorare le prestazioni del sistema. Ho rapidamente creato un prototipo per testare questa idea. Ha mostrato un aumento delle prestazioni superiore a dieci volte in vari micro-benchmark. La nostra idea era di compilare moduli Python in moduli C utilizzando Cython e di trasformare le annotazioni di tipo in controlli di tipo effettuati durante l'esecuzione del programma (di solito, le annotazioni di tipo vengono ignorate durante l'esecuzione dei programmi e sono utilizzate solo dai sistemi di controllo dei tipi). In effetti, stavamo pianificando di tradurre l'implementazione di mypy da Python a un linguaggio che fosse staticamente tipizzato, che apparisse (e, per la maggior parte, funzionasse) esattamente come Python. (Questa forma di migrazione tra linguaggi è diventata una sorta di tradizione per il progetto mypy. L'implementazione originale di mypy è stata scritta in Alore, poi c'è stata una ibridazione sintattica tra Java e Python).

L'orientamento sull'API delle estensioni CPython è stato fondamentale per non perdere opportunità nella gestione del progetto. Non abbiamo dovuto implementare una macchina virtuale o librerie di cui aveva bisogno mypy. Inoltre, avremmo comunque avuto accesso all'intero ecosistema Python, con a disposizione tutti gli strumenti (come pytest). Ciò significava che avremmo potuto continuare a utilizzare codice Python interpretato durante lo sviluppo, consentendoci di lavorare con una rapida modifica e test del codice, senza dover aspettare la compilazione. Sembra che siamo riusciti, per così dire, a stare su due sedie contemporaneamente, e questa situazione ci piaceva.

Il compilatore che abbiamo chiamato mypyc (poiché utilizza mypy per l'analisi dei tipi come frontend) si è rivelato un progetto molto riuscito. In generale, abbiamo ottenuto un incremento di circa quattro volte nella velocità di esecuzione frequente di mypy senza l'uso di caching. Lo sviluppo del nucleo del progetto mypyc ha richiesto al piccolo team composto da Michael Sullivan, Ivan Levkyvsky, Hugh Han e me circa 4 mesi di calendario. Questo lavoro è stato decisamente meno complesso di quanto sarebbe stato necessario per riscrivere mypy, ad esempio, in C++ o Go. Inoltre, i cambiamenti che abbiamo dovuto apportare al progetto sono stati notevolmente minori di quelli che avremmo dovuto fare riscrivendolo in un'altra lingua. Inoltre, speravamo di portare mypyc a un livello tale che altri programmatori di Dropbox potessero utilizzarlo per compilare e accelerare il proprio codice.

Per raggiungere un livello di performance simile, abbiamo dovuto adottare alcune soluzioni ingegneristiche interessanti. Ad esempio, il compilatore può accelerare l'esecuzione di molte operazioni grazie all'uso di strutture C rapide e a basso livello. Un invocazione di una funzione compilata viene tradotta in una chiamata di funzione C, e tale invocazione è notevolmente più veloce rispetto a quella di una funzione interpretata. Alcune operazioni, come la ricerca nei dizionari, continuavano a utilizzare normali chiamate all'API C di CPython, che dopo la compilazione si rivelavano solo leggermente più veloci. Siamo riusciti a eliminare l'onere aggiuntivo sulla sistema causato dall'interpretazione, ma questo ha portato soltanto a un modesto guadagno in termini di prestazioni.

Per identificare le operazioni più comuni considerate "lente", abbiamo eseguito il profiling del codice. Armati dei dati ottenuti, abbiamo cercato di "ottimizzare" mypyc affinché generasse codice C più veloce per tali operazioni, oppure di riscrivere il codice Python corrispondente utilizzando operazioni più veloci (e a volte non avevamo semplicemente una soluzione facile per un determinato problema). Riscrivere il codice Python si è frequentemente rivelato essere una soluzione più semplice rispetto all'implementazione dell'esecuzione automatica della stessa trasformazione nel compilatore. A lungo termine, volevamo automatizzare molte di queste trasformazioni, ma in quel momento eravamo focalizzati su come velocizzare mypy con il minimo sforzo. E mentre perseguivamo questo obiettivo, abbiamo preso alcune scorciatoie.

Continua...

Gentili lettori! Quali impressioni ti ha suscitato il progetto mypy quando hai scoperto la sua esistenza?

Il percorso per la verifica dei tipi di 4 milioni di righe di codice Python. Parte 2
Il percorso per la verifica dei tipi di 4 milioni di righe di codice Python. Parte 2

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster