Oggi pubblichiamo la seconda parte della traduzione del materiale su come Dropbox ha organizzato il controllo dei tipi di diverse milioni di righe di codice Python.
→
Supporto ufficiale dei tipi (PEP 484)
Abbiamo condotto i primi esperimenti significativi con mypy in Dropbox durante la Hack Week del 2014. La Hack Week è un evento organizzato da Dropbox per una settimana. In questo periodo, i dipendenti possono lavorare su qualsiasi cosa! Alcuni dei progetti tecnologici più famosi di Dropbox sono iniziati proprio in eventi come questo. A seguito di questo esperimento, abbiamo concluso che mypy appare promettente, anche se questo progetto non era ancora pronto per un utilizzo diffuso.
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 sui tipi per le funzioni, ma erano solo espressioni arbitrarie, senza una sintassi e semantica definite. Durante l'esecuzione del programma, queste annotazioni venivano per lo più ignorate. Dopo la Hack Week, abbiamo iniziato a lavorare sulla standardizzazione della semantica. Questo lavoro ha portato alla creazione di (questo documento è stato redatto congiuntamente da Guido van Rossum, Łukasz Langa e me.)
I nostri motivi possono essere visti da due punti di vista. In primo luogo, speravamo che l'intero ecosistema Python potesse adottare un approccio unificato all'uso delle indicazioni sui tipi (type hints - un termine utilizzato in Python come analogo delle 'annotazioni dei tipi'). Considerando i possibili rischi, sarebbe stato meglio rispetto all'uso di numerosi approcci reciprocamente incompatibili. In secondo luogo, volevamo discutere apertamente i meccanismi di annotazione dei tipi con molti membri della comunità Python. In parte, questo desiderio era dettato dal fatto che non volevamo sembrare 'dissidenti' rispetto alle idee di base del linguaggio agli occhi di una vasta scala di programmatori Python. È un linguaggio a tipizzazione dinamica, noto per la 'tipizzazione anatra'. All'inizio della comunità, non si poteva non avere un certo sospetto nei confronti dell'idea di tipizzazione statica. Ma questa mentalità alla fine si è affievolita, dopo che è diventato chiaro che la tipizzazione statica non sarebbe stata resa obbligatoria (e dopo che la gente ha compreso che è davvero utile).
Alla fine, la sintassi delle indicazioni sui tipi adottata si è rivelata molto simile a quella che all'epoca era supportata da mypy. Il documento PEP 484 è stato pubblicato 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 un importante traguardo nella storia di Python.
Inizio della migrazione
Alla fine del 2015, in Dropbox, è stato creato un team di tre persone per lavorare su mypy. Ne facevano parte Guido van Rossum, Greg Price e David Fisher. Da quel momento, la situazione ha cominciato a evolversi molto rapidamente. Il primo ostacolo alla crescita di mypy è stata la prestazione. Come ho accennato prima, in una fase iniziale dello sviluppo del progetto stavo considerando di trasferire l'implementazione di mypy sul linguaggio C, ma questa idea è stata per il momento depennata dalla lista. Siamo stati bloccati dall'utilizzo dell'interprete CPython, che non si distingue per la rapidità necessaria per strumenti come mypy. (Il progetto PyPy, un'implementazione alternativa di Python con compilatore JIT, non ci ha aiutato neanche.)
Fortunatamente, ci sono stati di aiuto alcuni miglioramenti algoritmici. Il primo potente "acceleratore" è stato l'implementazione del controllo incrementale. L'idea di questo miglioramento era semplice: se tutte le dipendenze di un modulo non sono cambiate dall'ultimo avvio di mypy, possiamo utilizzare, durante l'elaborazione delle dipendenze, i dati memorizzati nella cache durante la sessione precedente. Dobbiamo solo effettuare 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 è cambiata, mypy ha ritenuto che non fosse necessario ricontrollare 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 molte esecuzioni iterative di mypy, poiché le annotazioni vengono aggiunte gradualmente nel codice e migliorate man mano. La prima esecuzione di mypy era ancora molto lenta, poiché dovevano essere controllate 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 snapshot della cache per l'intera base di codice da un repository centralizzato. Quindi, esegue, utilizzando questo snapshot, il controllo incrementale. Questo ci ha fatto fare un altro notevole passo avanti nel migliorare le prestazioni di mypy.
È stato un periodo di veloce e naturale introduzione del sistema di controllo dei tipi in Dropbox. Entro la fine del 2016, avevamo già circa 420.000 righe di codice Python con annotazioni sui tipi. Molti utenti hanno accolto con entusiasmo il controllo dei tipi. In Dropbox, mypy è stato utilizzato da un numero crescente di squadre di sviluppatori.
Tutto allora sembrava buono, ma ci restava ancora molto da fare. Iniziammo a condurre indagini interne periodiche tra gli utenti per identificare i punti critici del progetto e capire quali questioni dovessero essere risolte per prime (questa pratica è ancora utilizzata in azienda ai giorni nostri). Le due questioni più importanti, come emerse chiaramente, erano due. La prima era necessaria una maggiore copertura del codice con i tipi, la seconda era necessario che mypy funzionasse più velocemente. Era assolutamente chiaro che il nostro lavoro per accelerare mypy e implementarlo nei progetti aziendali era ancora lontano dalla conclusione. Siamo quindi partiti per affrontare la risoluzione di queste due importanti questioni.
Maggiore produttività!
I controlli incrementali hanno accelerato mypy, ma questo strumento era ancora troppo lento. Molti controlli incrementali duravano circa un minuto. La causa di ciò erano le importazioni cicliche. Questo, probabilmente, non sorprenderà chi ha già lavorato con grandi basi di codice scritte in Python. Avevamo set di centinaia di moduli, ognuno dei quali importava indirettamente tutti gli altri. Se un qualsiasi file all'interno di un ciclo di importazione veniva modificato, a mypy toccava elaborare tutti i file che rientravano in quel ciclo, e spesso anche tutti i moduli che importavano moduli da quel ciclo. Uno di questi cicli era il famigerato
Abbiamo considerato la possibilità di "disintrecciare" le dipendenze circolari, ma non avevamo le risorse per farlo. C'era troppo codice con il quale non eravamo familiari. Alla fine, abbiamo adottato un approccio alternativo. Abbiamo deciso di fare in modo che mypy funzionasse rapidamente anche in presenza di "gomitoli di dipendenze". Abbiamo raggiunto questo obiettivo grazie a un demone mypy. Il demone è un processo server che implementa due interessanti funzionalità. In primo luogo, mantiene in memoria informazioni su tutta la base di codice. Ciò significa che ogni volta che viene eseguito mypy, non è necessario caricare i dati memorizzati nella cache relativi a migliaia di dipendenze importate. In secondo luogo, analizza attentamente, a livello di piccole unità strutturali, le dipendenze tra funzioni e altre entità. Ad esempio, se una funzione foo chiama la funzione bar, c'è una dipendenza foo di bar. Quando un file viene modificato, il demone prima elabora in isolamento solo il file modificato. Poi osserva le modifiche di quel file visibili dall'esterno, come le firme delle funzioni modificate. Il demone utilizza informazioni dettagliate sulle importazioni solo per ricontrollare quelle funzioni che utilizzano davvero la funzione modificata. Di solito, con questo approccio, le funzioni da controllare sono davvero poche.
Implementare tutto ciò non è stato facile, dato che l'implementazione originale di mypy era fortemente focalizzata su l'elaborazione di un file alla volta. Abbiamo dovuto affrontare molte situazioni limite, la cui insorgenza richiedeva verifiche ripetute quando veniva modificato qualcosa nel codice. Ad esempio, questo accade quando a una classe viene assegnata una nuova classe base. Dopo aver fatto ciò che volevamo, siamo riusciti a ridurre il tempo di esecuzione della maggior parte delle verifiche incrementali a pochi secondi. Ci è sembrato un grande successo.
Ancora più prestazioni!
Insieme alla memorizzazione nella cache remota di cui ho parlato sopra, il demone mypy ha praticamente risolto completamente i problemi che si presentano quando un programmatore esegue frequentemente un controllo dei tipi apportando modifiche a un numero ridotto di file. Tuttavia, le prestazioni del sistema, nel suo utilizzo meno favorevole, erano ancora lontane dall'essere ottimali. Un avvio pulito di mypy poteva richiedere più di 15 minuti. E questo era molto più di quanto avremmo tollerato. 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 prestazione e noi eravamo felici di andare incontro a questa esigenza.
Abbiamo deciso di tornare a una delle prime idee riguardanti mypy. In particolare, alla trasformazione del codice Python in codice C. Gli esperimenti con Cython (sistema che consente di tradurre il codice scritto in Python in codice C) non ci hanno fornito un'accelerazione visibile, quindi abbiamo deciso di riportare in vita l'idea di scrivere un nostro compilatore. Poiché il codice sorgente di mypy (scritto in Python) conteneva già tutte le necessarie annotazioni sui tipi, ci sembrava valido tentare di utilizzare queste annotazioni per accelerare il funzionamento del sistema. Ho creato rapidamente un prototipo per testare questa idea. Ha mostrato un incremento della prestazione di oltre 10 volte in diversi micro-benchmark. La nostra idea era quella di compilare i moduli Python in moduli C utilizzando Cython, e di trasformare le annotazioni sui tipi in controlli dei tipi eseguiti durante l'esecuzione del programma (normalmente, le annotazioni sui tipi vengono ignorate durante l'esecuzione dei programmi e utilizzate solo dai sistemi di verifica dei tipi). In sostanza, pianificavamo di trasferire 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 interlinguistica è diventata una sorta di tradizione nel progetto mypy. L'implementazione iniziale di mypy è stata scritta in Alore, poi è seguito un ibrido sintattico tra Java e Python.)
L'orientamento verso l'API delle estensioni CPython è stato fondamentale per non perdere opportunità nella gestione del progetto. Non è stato necessario implementare una macchina virtuale o librerie di cui mypy necessitava. Inoltre, avremmo ancora avuto accesso all'intero ecosistema Python e a tutti gli strumenti (come pytest). Ciò significava che avremmo potuto continuare a utilizzare codice Python interpretato durante lo sviluppo, permettendoci di continuare a lavorare con un sistema di modifica e test del codice molto veloce, senza dover aspettare la compilazione del codice. Sembra che siamo stati in grado, così da dire, di rimanere su due sedia contemporaneamente, e a noi piaceva.
Il compilatore che abbiamo chiamato mypyc (poiché utilizza mypy come frontend per l'analisi dei tipi) si è rivelato un progetto piuttosto di successo. In generale, abbiamo raggiunto un'accelerazione di circa 4 volte nei frequenti avvii di mypy senza l'uso di caching. Lo sviluppo del nucleo del progetto mypyc ha richiesto a un piccolo team, composto da Michael Sullivan, Ivan Levkivskyi, Hugh Han e me, circa 4 mesi di calendario. Questo lavoro è stato decisamente meno vasto rispetto a quello che sarebbe stato necessario per riscrivere mypy, ad esempio, in C++ o in Go. Inoltre, le modifiche al progetto che abbiamo dovuto apportare sono state notevolmente inferiori a quelle necessarie per una riscrittura in un'altra lingua. Inoltre, speravamo di poter portare mypyc a un livello tale da poter essere utilizzato per compilare e accelerare il proprio codice da altri programmatori di Dropbox.
Per raggiungere un simile livello di prestazioni, abbiamo dovuto applicare alcune soluzioni ingegneristiche interessanti. Ad esempio, il compilatore può accelerare l'esecuzione di molte operazioni grazie all'uso di rapide costruzioni C a basso livello. Ad esempio, la chiamata a una funzione compilata viene tradotta in una chiamata a una funzione C. Questa chiamata viene eseguita molto più velocemente rispetto a una chiamata a una funzione interpretata. Alcune operazioni, come la ricerca nei dizionari, si sono ancora ridotte all'uso delle normali chiamate all'API C da CPython, che dopo la compilazione si sono rivelate solo leggermente più veloci. Siamo riusciti a liberarci dell'overhead creato dall'interpretazione, ma questo ha portato solo a un piccolo guadagno in termini di prestazioni.
Per identificare le operazioni "lente" più comuni, abbiamo eseguito il profiling del codice. Armati dei dati ottenuti, abbiamo cercato di "ottimizzare" mypyc in modo che generasse un 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 ciascun problema). Riscrivere il codice Python si è spesso dimostrato una soluzione più facile al problema rispetto all'implementazione di un'esecuzione automatica della stessa trasformazione nel compilatore. A lungo termine, volevamo automatizzare molte di queste trasformazioni, ma in quel momento ci siamo concentrati nel far avanzare mypy con il minimo sforzo. E, procedendo verso questo obiettivo, abbiamo tagliato alcuni angoli.
Continua…
Gentili lettori! Quali sono state le tue impressioni sul progetto mypy quando hai scoperto la sua esistenza?
Fonte: habr.com
