Hoy publicamos la segunda parte de la traducción del material sobre cómo Dropbox organizó el control de tipos de varios millones de líneas de código Python.

→
Leer la primera parteSoporte de tipo formal (PEP 484)
Hicimos el primer experimento serio con mypy en Dropbox durante la Hack Week 2014. Hack Week es un evento realizado por Dropbox durante una semana. ¡En este momento, los empleados pueden trabajar en cualquier cosa! Algunos de los proyectos tecnológicos más famosos de Dropbox comenzaron en eventos similares. Como resultado de este experimento, llegamos a la conclusión de que mypy parece prometedor, aunque este proyecto aún no estaba listo para su uso generalizado.
En ese momento, la idea de estandarizar los sistemas de sugerencias para los tipos de Python estaba en el aire. Como dije, comenzando con Python 3.0, podría usar anotaciones de tipo para funciones, pero estas eran solo expresiones arbitrarias, sin sintaxis y semántica específicas. Durante la ejecución del programa, estas anotaciones, en su mayor parte, simplemente se ignoraron. Después de Hack Week, comenzamos a trabajar en la estandarización de la semántica. Este trabajo condujo a la aparición de
PEP 484 (Guido van Rossum, Lukas Langa y yo colaboramos en este documento).
Nuestros motivos pueden verse desde dos lados. Primero, esperábamos que todo el ecosistema de Python pudiera adoptar un enfoque general para usar sugerencias de tipo (sugerencias de tipo es un término utilizado en Python como un análogo de "anotaciones de tipo"). Esto, dados los posibles riesgos, sería mejor que utilizar muchos enfoques mutuamente incompatibles. En segundo lugar, queríamos discutir abiertamente los mecanismos de anotación de tipo con muchos miembros de la comunidad Python. En parte, este deseo fue dictado por el hecho de que no querríamos parecer "apóstatas" a partir de las ideas básicas del lenguaje a los ojos de las grandes masas de programadores de Python. Es un lenguaje escrito dinámicamente conocido como "escritura de pato". En la comunidad, al principio, una actitud algo sospechosa hacia la idea del tipeo estático no pudo evitar surgir. Pero esta actitud finalmente se debilitó, después de que quedó claro que la escritura estática no estaba planificada como obligatoria (y después de que la gente se dio cuenta de que era realmente útil).
La sintaxis resultante para las sugerencias de tipo fue muy similar a la que mypy admitía en ese momento. PEP 484 salió con Python 3.5 en 2015. Python ya no era un lenguaje que solo admitía la escritura dinámica. Me gusta pensar en este evento como un hito importante en la historia de Python.
Inicio de la migración
A finales de 2015, se creó un equipo de tres personas en Dropbox para trabajar en mypy. Incluía a Guido van Rossum, Greg Price y David Fisher. A partir de ese momento, la situación comenzó a desarrollarse extremadamente rápido. El primer obstáculo para el crecimiento mypy fue el rendimiento. Como ya indiqué anteriormente, en el período inicial del desarrollo del proyecto, estaba pensando en traducir la implementación de mypy a C, pero esta idea se ha eliminado de las listas hasta ahora. Estamos atrapados con el hecho de que utilizamos el intérprete de CPython para iniciar el sistema, que no es lo suficientemente rápido para herramientas como mypy. (El proyecto PyPy, una implementación alternativa de Python con un compilador JIT, tampoco nos ayudó).
Afortunadamente, aquí algunas mejoras algorítmicas vinieron en nuestra ayuda. El primer poderoso "acelerador" fue la implementación de la verificación incremental. La idea de esta mejora era simple: si todas las dependencias del módulo no han cambiado desde el lanzamiento anterior de mypy, entonces podemos usar los datos almacenados en caché durante la sesión anterior mientras trabajamos con dependencias. Todo lo que teníamos que hacer era verificar el tipo en los archivos modificados y en los archivos que dependían de ellos. Mypy fue incluso un poco más allá: si la interfaz externa del módulo no cambiaba, mypy pensó que otros módulos que importan este módulo no deberían volver a comprobarse.
La validación incremental nos ha ayudado enormemente a anotar grandes volúmenes de código existente. El hecho es que este proceso generalmente implica muchas ejecuciones iterativas de mypy, ya que las anotaciones se agregan gradualmente al código y se mejoran gradualmente. El primer lanzamiento de mypy aún fue muy lento, ya que cuando lo ejecutó, tuvo que verificar muchas dependencias. Luego, en aras de mejorar la situación, implementamos un mecanismo de almacenamiento en caché remoto. Si mypy detecta que el caché local probablemente está desactualizado, descarga la instantánea de caché actual para toda la base de código desde un repositorio centralizado. Luego realiza una verificación incremental utilizando esta instantánea. Este es otro gran paso que nos ha llevado a aumentar la productividad mypy.
Este fue un período de introducción rápida y natural del sistema de verificación de tipo Dropbox. A finales de 2016, ya teníamos aproximadamente 420,000 líneas de código Python con anotaciones de tipo. Muchos usuarios estaban entusiasmados con la verificación de tipos. Dropbox mypy ha sido utilizado por más y más equipos de desarrollo.
Todo se veía bien entonces, pero todavía teníamos mucho que hacer. Comenzamos a realizar encuestas periódicas de usuarios internos para identificar las áreas problemáticas del proyecto y comprender qué problemas deben abordarse primero (esta práctica se usa hoy en la empresa). Lo más importante, como quedó claro, fueron dos tareas. El primero, necesitabas más cobertura de código con tipos, el segundo, tenías que hacer que mypy funcionara más rápido. Estaba perfectamente claro que nuestro trabajo para acelerar mypy y su implementación en los proyectos de la compañía aún estaba lejos de completarse. Nosotros, plenamente conscientes de la importancia de estas dos tareas, tomamos su solución.
Más rendimiento!
Las comprobaciones incrementales aceleraron mypy, pero esta herramienta todavía no era lo suficientemente rápida. Muchos controles incrementales duraron aproximadamente un minuto. La razón de esto fueron las importaciones cíclicas. Esto probablemente no sorprenderá a nadie que haya trabajado con grandes bases de código escritas en Python. Teníamos conjuntos de cientos de módulos, cada uno de los cuales importó indirectamente todos los demás. Si algún archivo en el ciclo de importación resultó ser cambiado, mypy tuvo que procesar todos los archivos incluidos en este ciclo y, a menudo, también los módulos que importan módulos de este ciclo. Uno de esos ciclos fue la infame "maraña de dependencias", que causó muchos problemas en Dropbox. Una vez que esta estructura contenía varios cientos de módulos, mientras se importaba, directa o indirectamente, muchas pruebas, también se usaba en el código de producción.
Consideramos la posibilidad de "desentrañar" dependencias cíclicas, pero no teníamos los recursos para hacerlo. Había demasiado código con el que no estábamos familiarizados. Como resultado, tomamos un enfoque alternativo. Decidimos hacer que mypy funcionara rápido incluso si hubiera "bolas de dependencia". Logramos esto con el demonio mypy. Un daemon es un proceso de servidor que implementa dos características interesantes. En primer lugar, guarda en la memoria información sobre toda la base del código. Esto significa que cada vez que ejecuta mypy, no tiene que descargar datos en caché relacionados con miles de dependencias importadas. En segundo lugar, cuidadosamente, a nivel de pequeñas unidades estructurales, analiza las relaciones entre funciones y otras entidades. Por ejemplo, si la función
foo
llama a la
bar
funciones, entonces hay una dependencia de
foo
en la
bar
. Cuando se cambia un archivo, el daemon primero, de forma aislada, procesa solo el archivo cambiado. Luego observa los cambios en este archivo que son visibles desde el exterior, como las firmas de funciones modificadas. El daemon usa información de importación detallada solo para verificar las funciones que realmente usan la función modificada. Por lo general, con este enfoque, se deben verificar muy pocas funciones.
Implementar todo esto no fue fácil, ya que la implementación original de mypy se centró en gran medida en procesar un archivo a la vez. Tuvimos que lidiar con muchas situaciones límite, cuya ocurrencia requirió verificaciones repetidas en los casos en que algo cambió en el código. Por ejemplo, esto sucede cuando se asigna una nueva clase base a una clase. Después de hacer lo que queríamos, pudimos reducir el tiempo de ejecución de la mayoría de las comprobaciones incrementales a unos pocos segundos. Nos pareció una gran victoria.
Más rendimiento!
Junto con el almacenamiento en caché remoto, que describí anteriormente, el demonio mypy resolvió casi por completo los problemas que surgen cuando el programador a menudo ejecuta la verificación de tipos, realizando cambios en una pequeña cantidad de archivos. Sin embargo, el rendimiento del sistema en la variante menos favorable de su uso aún estaba lejos de ser óptimo. Un comienzo limpio de mypy podría tomar más de 15 minutos. Y fue mucho más de lo que nos gustaría. Cada semana, la situación empeoraba, ya que los programadores continuaron escribiendo código nuevo y agregando anotaciones al código existente. Nuestros usuarios todavía anhelaban un mayor rendimiento, pero nos alegramos de estar listos para satisfacerlos.
Decidimos volver a una de mis primeras ideas sobre mypy. A saber, la conversión de código Python a código C. Los experimentos con Cython (este es un sistema que le permite traducir el código Python en código C) no nos dieron ninguna aceleración visible, por lo que decidimos revivir la idea de escribir nuestro propio compilador. Dado que la base de código mypy (escrita en Python) ya contenía todas las anotaciones de tipo necesarias, un intento de usar estas anotaciones para acelerar el sistema parecía valer la pena. Rápidamente creé un prototipo para probar esta idea. Mostró en varios micro puntos de referencia un aumento de más de 10 veces en la productividad. Nuestra idea era compilar módulos de Python en módulos C utilizando las herramientas de Cython, y convertir las anotaciones de tipo en verificaciones de tipo realizadas en el tiempo de ejecución (por lo general, las anotaciones de tipo se ignoran en el tiempo de ejecución y solo las usan los sistemas de verificación de tipo ) De hecho, planeamos traducir la implementación mypy de Python a un lenguaje creado de forma estática, que se vería (y, en su mayor parte, funcionaría) exactamente como Python. (Este tipo de migración entre idiomas se ha convertido en una tradición del proyecto mypy. La implementación inicial de mypy se escribió en Alore, luego hubo un híbrido sintáctico de Java y Python).
Centrarse en la API de extensión CPython fue la clave para no perder las capacidades de gestión de proyectos. No necesitábamos implementar una máquina virtual ni ninguna biblioteca que mypy necesitara. Además, todo el ecosistema de Python aún estaría disponible para nosotros, todas las herramientas (como pytest) estarían disponibles. Esto significaba que podíamos seguir usando el código Python interpretado durante el desarrollo, lo que nos permitiría seguir trabajando usando un esquema muy rápido para realizar cambios en el código y probarlo, en lugar de esperar a que el código se compile. Parecía que pudiéramos sentarnos en dos sillas, por así decirlo, y nos gustó.
El compilador, que llamamos mypyc (ya que usa mypy como interfaz para el análisis de tipos), resultó ser un proyecto muy exitoso. En general, logramos una aceleración aproximadamente 4 veces más rápida de las ejecuciones frecuentes de mypy sin almacenamiento en caché. El desarrollo del núcleo del proyecto mypyc tomó alrededor de 4 meses calendario de un pequeño equipo que incluía a Michael Sullivan, Ivan Levkivsky, Hugh Han y yo. Esta cantidad de trabajo fue mucho menos ambiciosa de lo que se necesitaría para reescribir mypy, por ejemplo, en C ++ o Go. Y tuvimos que hacer muchos menos cambios en el proyecto de los que tendríamos que hacer al reescribirlo en otro idioma. También esperábamos poder llevar mypyc a un nivel tal que otros programadores de Dropbox pudieran usarlo para compilar y acelerar su código.
Para lograr este nivel de rendimiento, tuvimos que aplicar algunas soluciones de ingeniería interesantes. Entonces, el compilador puede acelerar muchas operaciones mediante el uso de construcciones C rápidas de bajo nivel. Por ejemplo, una llamada a una función compilada se traduce en una llamada a una función C. Y dicha llamada se realiza mucho más rápido que llamar a una función interpretada. Algunas operaciones, como las búsquedas en el diccionario, todavía se reducían al uso de llamadas regulares de C-API desde CPython, que después de la compilación resultó ser solo un poco más rápido. Pudimos deshacernos de la carga adicional en el sistema creada por la interpretación, pero esto en este caso solo proporcionó una pequeña ganancia en términos de rendimiento.
Para identificar las operaciones "lentas" más comunes, realizamos la creación de perfiles de código. Armados con los datos obtenidos, tratamos de ajustar mypyc para que generara un código C más rápido para tales operaciones, o reescribir el código Python correspondiente usando operaciones más rápidas (y a veces simplemente no teníamos una solución lo suficientemente simple para eso u otro problema). Reescribir el código de Python a menudo resultó ser una solución más fácil al problema que implementar la misma transformación automáticamente en el compilador. A la larga, queríamos automatizar muchas de estas transformaciones, pero en ese momento buscamos acelerar mypy con un mínimo de esfuerzo. Y nosotros, avanzando hacia este objetivo, cortamos varias esquinas.
Continuará ...
Estimados lectores! ¿Cuáles fueron sus impresiones sobre el proyecto mypy cuando se enteró de su existencia?
