El camino para verificar tipos en 4 millones de líneas de código Python. Parte 2

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

El camino para verificar tipos en 4 millones de líneas de código Python. Parte 2

→ Leer la primera parte

Soporte oficial de tipos (PEP 484)

Realizamos nuestras primeras experiencias serias con mypy en Dropbox durante la Hack Week de 2014. La Hack Week es un evento que se lleva a cabo en Dropbox durante una semana. Durante este tiempo, los empleados pueden trabajar en lo que quieran. Algunos de los proyectos tecnológicos más emblemáticos de Dropbox comenzaron precisamente en estos eventos. Como resultado de este experimento, concluimos que mypy parece prometedor, aunque el proyecto aún no estaba listo para un uso general.

En ese momento, había en el aire la idea de estandarizar los sistemas de sugerencias de tipos en Python. Como mencioné, a partir de Python 3.0 se podían usar anotaciones de tipo para funciones, pero eran solo expresiones arbitrarias, sin una sintaxis y semántica definidas. Durante la ejecución del programa, estas anotaciones eran, en su mayoría, simplemente ignoradas. Después de la Hack Week, comenzamos a trabajar en la estandarización de la semántica. Este trabajo llevó a la aparición de PEP 484 (este documento fue elaborado en conjunto por Guido van Rossum, Łukasz Langa y yo).

Nuestros motivos se pueden ver desde dos perspectivas. Primero, esperábamos que todo el ecosistema de Python pudiera adoptar un enfoque común en el uso de sugerencias de tipos (type hints, un término usado en Python como un análogo a "anotaciones de tipos"). Esto, considerando los posibles riesgos, sería mejor que usar una multitud de enfoques mutuamente incompatibles. En segundo lugar, queríamos discutir abiertamente los mecanismos de anotación de tipos con muchos miembros de la comunidad de Python. En parte, este deseo fue dictado por el hecho de que no queríamos parecer "herejes" de las ideas básicas del lenguaje ante las masas de programadores de Python. Este es un lenguaje de tipado dinámico, conocido por su "tipado de pato". En la comunidad, al principio, surgió una cierta desconfianza hacia la idea de la tipificación estática. Pero esa actitud se debilitó con el tiempo, una vez que quedó claro que no se planeaba hacer obligatoria la tipificación estática (y después de que las personas se dieron cuenta de que realmente era útil).

La sintaxis final de las sugerencias de tipos fue muy similar a la que mypy soportaba en ese momento. El documento PEP 484 fue lanzado junto con Python 3.5 en 2015. Python dejó de ser un lenguaje que solo soportaba tipado dinámico. Me gusta pensar en este evento como un hito significativo en la historia de Python.

Inicio de la migración

A finales de 2015, se formó 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 en el camino del crecimiento de mypy fue el rendimiento. Como mencioné anteriormente, en las primeras etapas del desarrollo del proyecto consideré traducir la implementación de mypy al lenguaje C, pero esta idea fue descartada por el momento. Estábamos atrapados en el hecho de que para ejecutar el sistema se utilizaba el intérprete CPython, que no ofrecía la velocidad suficiente para herramientas como mypy. (El proyecto PyPy, una implementación alternativa de Python con compilador JIT, tampoco nos ayudó.)

Afortunadamente, algunos avances algorítmicos vinieron en nuestro auxilio. El primer y poderoso "acelerador" fue la implementación de la verificación incremental. La idea de esta mejora era simple: si todas las dependencias de un módulo no habían cambiado desde la última ejecución de mypy, podíamos utilizar, durante el trabajo con las dependencias, los datos en caché de la sesión anterior. Solo necesitábamos realizar una verificación de tipos en los archivos modificados y en aquellos que dependían de ellos. Mypy fue incluso un poco más lejos: si la interfaz externa del módulo no había cambiado, mypy consideraba que otros módulos que importaban este módulo no necesitaban ser verificados nuevamente.

La verificación incremental nos ayudó mucho al anotar grandes volúmenes de código existente. El hecho es que este proceso generalmente implica múltiples ejecuciones iterativas de mypy, ya que las anotaciones se agregan gradualmente al código y se mejoran poco a poco. La primera ejecución de mypy aún era muy lenta, ya que durante su ejecución había que verificar muchas dependencias. Entonces, con el objetivo de mejorar la situación, implementamos un mecanismo de almacenamiento en caché remoto. Si mypy detecta que la caché local probablemente está desactualizada, descarga la instantánea actual de la caché para toda la base de código desde un repositorio centralizado. Luego, ejecuta, usando esta instantánea, una verificación incremental. Esto nos llevó otro paso considerable hacia el aumento del rendimiento de mypy.

Fue un período de adopción rápida y natural del sistema de verificación de tipos en Dropbox. Para finales de 2016, ya teníamos aproximadamente 420,000 líneas de código Python con anotaciones de tipos. Muchos usuarios recibieron con entusiasmo la verificación de tipos. En Dropbox, mypy fue utilizado cada vez más por equipos de desarrolladores.

Todo parecía ir bien, pero aún teníamos mucho por hacer. Comenzamos a realizar encuestas internas periódicas a los usuarios para identificar los problemas del proyecto y entender cuáles preguntas debían resolverse primero (esta práctica se utiliza en la empresa hasta el día de hoy). Como se hizo evidente, las dos tareas más importantes eran. Primero, necesitábamos más cobertura de código con tipos; segundo, necesitábamos que mypy funcionara más rápido. Era completamente claro que nuestro trabajo para acelerar mypy y su implementación en los proyectos de la empresa aún estaba lejos de completarse. Con plena conciencia de la importancia de estas dos tareas, nos pusimos manos a la obra para resolverlas.

¡Más rendimiento!

Las verificaciones incrementales aceleraron mypy, pero esta herramienta aún no era lo suficientemente rápida. Muchas verificaciones incrementales duraban alrededor de un minuto. La causa de esto eran las importaciones cíclicas. Esto probablemente no sorprenderá a aquellos que han trabajado con grandes bases de código escritas en Python. Teníamos conjuntos de cientos de módulos, cada uno de los cuales importaba indirectamente a todos los demás. Si algún archivo dentro del ciclo de importación era modificado, mypy tenía que procesar todos los archivos en ese ciclo, y frecuentemente incluso cualquier módulo que importara módulos de ese ciclo. Uno de esos ciclos era el infame

Consideramos la posibilidad de "desenredar" las dependencias cíclicas, pero no teníamos los recursos para hacerlo. Había demasiado código con el que no estábamos familiarizados. Al final, optamos por un enfoque alternativo. Decidimos hacer que mypy funcionara rápidamente incluso con " ". Alcanzamos este objetivo mediante un demonio mypy. Un demonio es un proceso del servidor que implementa dos características interesantes. Primero, mantiene en memoria información sobre toda la base de código. Esto significa que en cada ejecución de mypy no es necesario cargar datos en caché relativos a miles de dependencias importadas. En segundo lugar, analiza cuidadosamente, a nivel de unidades estructurales pequeñas, las dependencias entre funciones y otras entidades. Por ejemplo, si la función foo llama a la función bar, entonces hay una dependencia foo desde bar. Cuando un archivo cambia, el demonio primero procesa en aislamiento solo el archivo modificado. Luego observa los cambios en ese archivo que son visibles externamente, como las firmas de funciones que han cambiado. El demonio utiliza información detallada sobre las importaciones solo para volver a verificar aquellas funciones que realmente utilizan la función modificada. Usualmente, con este enfoque, hay que verificar muy pocas funciones.

La implementación de todo esto no fue una tarea sencilla, ya que la implementación original de mypy estaba muy orientada al procesamiento de un solo archivo a la vez. Tuvimos que enfrentarnos a numerosas situaciones límite, cuya aparición requería verificaciones repetidas cada vez que se realizaba un cambio 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, logramos reducir el tiempo de ejecución de la mayoría de las comprobaciones incrementales a unos pocos segundos. Para nosotros, esto fue una gran victoria.

¡Aún más rendimiento!

Junto con la caché remota, de la que hablé antes, el demonio de mypy prácticamente resolvió los problemas que surgían cuando un programador ejecutaba a menudo la verificación de tipos, haciendo cambios en un pequeño número de archivos. Sin embargo, el rendimiento del sistema en el escenario menos favorable seguía estando lejos de ser óptimo. Un inicio limpio de mypy podía llevar más de 15 minutos. Y eso era mucho más de lo que nos hubiera gustado. Cada semana la situación empeoraba, ya que los programadores seguían escribiendo nuevo código y añadiendo anotaciones al código existente. Nuestros usuarios aún deseaban más rendimiento, y nosotros estábamos encantados de complacerlos.

Decidimos volver a una de las ideas tempranas respecto a mypy. Específicamente, a la transformación de código Python en código C. Los experimentos con Cython (un sistema que permite traducir código escrito en Python a código C) no nos dieron ningún aumento de velocidad visible, por lo que decidimos revivir la idea de escribir nuestro propio compilador. Dado que la base de código de mypy (escrita en Python) ya contenía todas las anotaciones de tipo necesarias, nos pareció una buena idea utilizar estas anotaciones para acelerar el funcionamiento del sistema. Rápidamente creé un prototipo para comprobar esta idea. Este mostró en varios micro-benchmarks un crecimiento de rendimiento de más de 10 veces. Nuestra idea era compilar módulos de Python en módulos C utilizando Cython, y convertir las anotaciones de tipo en verificaciones de tipo que se realizarían en tiempo de ejecución del programa (normalmente, las anotaciones de tipo se ignoran durante la ejecución de los programas y son utilizadas solo por sistemas de verificación de tipos). De hecho, planeábamos trasladar la implementación de mypy de Python a un lenguaje que hubiera sido creado con tipado estático, que se vería (y, en su mayor parte, funcionaría) exactamente como Python. (Este tipo de migración entre lenguajes se convirtió en algo así como una tradición del proyecto mypy. La implementación original de mypy fue escrita en Alore, y luego fue un híbrido sintáctico de Java y Python).

La orientación hacia la API de extensiones de CPython fue clave para no perder capacidades en la gestión del proyecto. No necesitábamos implementar una máquina virtual o cualquier biblioteca que mypy requiriera. Además, aún tendríamos acceso a todo el ecosistema de Python, y estarían disponibles todas las herramientas (como pytest). Esto significaba que podríamos seguir utilizando código Python interpretado a lo largo del desarrollo, lo que nos permitiría continuar trabajando con un esquema de modificación y prueba de código muy rápido, en lugar de esperar a que el código se compilara. Parecía que estábamos logrando, por así decirlo, mantenernos en dos sillas al mismo tiempo, y nos gustaba.

El compilador que hemos llamado mypyc (ya que utiliza mypy como frontend para el análisis de tipos) ha demostrado ser un proyecto bastante exitoso. En general, logramos aproximadamente una aceleración de cuatro veces en las ejecuciones frecuentes de mypy sin el uso de caché. El desarrollo del núcleo del proyecto mypyc tomó alrededor de 4 meses calendario para un pequeño equipo que incluía a Michael Sullivan, Ivan Levkiysky, Hugh Hahn y yo. Este volumen de trabajo fue mucho menos extenso que el que habría requerido reescribir mypy, por ejemplo, en C++ o Go. Además, las modificaciones que hicimos en el proyecto fueron considerablemente menores que las que habríamos tenido que realizar al reescribirlo en otro lenguaje. También teníamos la esperanza de que pudiéramos llevar mypyc a un nivel en el que otros programadores de Dropbox pudieran utilizarlo para compilar y acelerar su código.

Para alcanzar un nivel de rendimiento similar, tuvimos que implementar algunas soluciones de ingeniería interesantes. De este modo, el compilador puede acelerar la ejecución de muchas operaciones gracias al uso de construcciones C de bajo nivel rápidas. Por ejemplo, la llamada a una función compilada se traduce en una llamada a una función C. Y una llamada así se ejecuta mucho más rápido que una llamada a una función interpretada. Algunas operaciones, como la búsqueda en diccionarios, aún dependían de llamados al C-API de CPython, que después de la compilación resultaban solo un poco más rápidos. Logramos eliminara la carga adicional que genera la interpretación, pero en este caso solo se tradujo en una pequeña mejora en rendimiento.

Para identificar las operaciones «lentas» más comunes, realizamos un perfilado del código. Con los datos obtenidos, intentamos ajustar mypyc para que generara un código C más rápido para este tipo de operaciones, o reescribir el código Python correspondiente utilizando operaciones más rápidas (y a veces simplemente no teníamos una solución fácil para algún problema en particular). Reescribir el código Python a menudo resultó ser una solución más sencilla al problema que implementar la transformación automática de la misma en el compilador. A largo plazo, queríamos automatizar muchas de estas transformaciones, pero en ese momento nos centramos en acelerar mypy con el mínimo esfuerzo. Y, al avanzar hacia este objetivo, recortamos algunas esquinas.

Continuará...

¡Estimados lectores! ¿Qué impresiones te causó el proyecto mypy cuando supiste de su existencia?

El camino para verificar tipos en 4 millones de líneas de código Python. Parte 2
El camino para verificar tipos en 4 millones de líneas de código Python. Parte 2

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster