El camino para verificar el tipo de 4 millones de líneas de código Python. Parte 1

Hoy llamamos su atención sobre la primera parte de la traducción del material sobre cómo Dropbox maneja el control de tipos del código Python.

El camino para verificar el tipo de 4 millones de líneas de código Python. Parte 1

Dropbox escribe mucho en Python. Es un lenguaje que utilizamos con mucha frecuencia, tanto para servicios de back-end como para aplicaciones de cliente de escritorio. También usamos mucho Go, TypeScript y Rust, pero Python es nuestro lenguaje principal. Teniendo en cuenta nuestra escala, y estamos hablando de millones de líneas de código Python, resultó que la escritura dinámica de dicho código complicó innecesariamente su comprensión y comenzó a afectar seriamente la productividad laboral. Para mitigar este problema, hemos comenzado a hacer una transición gradual de nuestro código a la verificación de tipos estáticos mediante mypy. Este es probablemente el sistema de verificación de tipos independiente más popular para Python. Mypy es un proyecto de código abierto, sus principales desarrolladores trabajan en Dropbox.

Dropbox fue una de las primeras empresas en implementar la verificación de tipos estáticos en código Python a esta escala. Mypy se utiliza en miles de proyectos en estos días. Esta herramienta innumerables veces, como dicen, "probada en batalla". Hemos recorrido un largo camino para llegar a donde estamos ahora. En el camino, hubo muchas empresas fallidas y experimentos fallidos. Esta publicación cubre la historia de la verificación de tipos estáticos en Python, desde sus comienzos difíciles como parte de mi proyecto de investigación, hasta el día de hoy, cuando la verificación de tipos y las sugerencias de tipos se han vuelto comunes para innumerables desarrolladores que escriben en Python. Estos mecanismos ahora son compatibles con muchas herramientas, como IDE y analizadores de código.

Leer la segunda parte

¿Por qué es necesaria la verificación de tipos?

Si alguna vez ha usado Python tipeado dinámicamente, es posible que tenga cierta confusión sobre por qué ha habido tanto alboroto en torno a la tipificación estática y mypy últimamente. O tal vez te gusta Python precisamente por su escritura dinámica, y lo que está sucediendo simplemente te molesta. La clave del valor de la escritura estática es la escala de las soluciones: cuanto más grande sea su proyecto, más se inclinará hacia la escritura estática y, al final, más la necesitará realmente.

Supongamos que cierto proyecto ha alcanzado el tamaño de decenas de miles de líneas y resulta que varios programadores están trabajando en él. Mirando un proyecto similar, basándonos en nuestra experiencia, podemos decir que comprender su código será la clave para mantener la productividad de los desarrolladores. Sin anotaciones de tipo, puede ser difícil averiguar, por ejemplo, qué argumentos pasar a una función o qué tipos puede devolver una función. Aquí hay preguntas típicas que a menudo son difíciles de responder sin usar anotaciones de tipo:

  • ¿Puede esta función devolver None?
  • ¿Cuál debería ser este argumento? items?
  • ¿Cuál es el tipo de atributo? id: int Lo es, str, o tal vez algún tipo personalizado?
  • ¿Este argumento debería ser una lista? ¿Es posible pasarle una tupla?

Si observa el siguiente fragmento de código con anotaciones de tipo e intenta responder preguntas similares, resulta que esta es la tarea más simple:

class Resource:
    id: bytes
    ...
    def read_metadata(self, 
                      items: Sequence[str]) -> Dict[str, MetadataItem]:
        ...

  • read_metadata no regresa None, ya que el tipo de retorno no es Optional[…].
  • Argumento items es una secuencia de líneas. No se puede iterar aleatoriamente.
  • Atributo id es una cadena de bytes.

En un mundo ideal, uno esperaría que todas esas sutilezas se describieran en la documentación integrada (docstring). Pero la experiencia da muchos ejemplos del hecho de que dicha documentación a menudo no se observa en el código con el que tiene que trabajar. Incluso si dicha documentación está presente en el código, no se puede contar con su absoluta corrección. Esta documentación puede ser vaga, inexacta y abierta a malentendidos. En grandes equipos o grandes proyectos, este problema puede volverse extremadamente agudo.

Si bien Python sobresale en las etapas iniciales o intermedias de los proyectos, en algún momento los proyectos exitosos y las empresas que usan Python pueden enfrentar la pregunta vital: "¿Deberíamos reescribir todo en un lenguaje de tipado estático?".

Los sistemas de comprobación de tipos como mypy resuelven el problema anterior proporcionando al desarrollador un lenguaje formal para describir tipos y comprobando que las declaraciones de tipos coincidan con la implementación del programa (y, opcionalmente, comprobando su existencia). En general, podemos decir que estos sistemas ponen a nuestra disposición algo así como una documentación cuidadosamente revisada.

El uso de tales sistemas tiene otras ventajas, y ya son completamente no triviales:

  • El sistema de verificación de tipos puede detectar algunos errores pequeños (y no tan pequeños). Un ejemplo típico es cuando se olvidan de procesar un valor None o alguna otra condición especial.
  • La refactorización de código se simplifica enormemente porque el sistema de verificación de tipos suele ser muy preciso sobre qué código debe cambiarse. Al mismo tiempo, no necesitamos esperar una cobertura de código del 100% con pruebas, lo que, en cualquier caso, generalmente no es factible. No necesitamos profundizar en las profundidades del seguimiento de la pila para descubrir la causa del problema.
  • Incluso en proyectos grandes, mypy a menudo puede realizar una verificación de tipo completa en una fracción de segundo. Y la ejecución de las pruebas suele tardar decenas de segundos o incluso minutos. El sistema de verificación de tipos le brinda al programador una respuesta instantánea y le permite hacer su trabajo más rápido. Ya no necesita escribir pruebas unitarias frágiles y difíciles de mantener que reemplazan entidades reales con simulacros y parches solo para obtener resultados de pruebas de código más rápido.

Los IDE y los editores, como PyCharm o Visual Studio Code, usan el poder de las anotaciones de tipo para proporcionar a los desarrolladores la finalización del código, el resaltado de errores y la compatibilidad con construcciones de lenguaje de uso común. Y estos son solo algunos de los beneficios de escribir. Para algunos programadores, todo esto es el principal argumento a favor de escribir. Esto es algo que se beneficia inmediatamente después de la implementación. Este caso de uso para tipos no requiere un sistema de verificación de tipos separado como mypy, aunque debe tenerse en cuenta que mypy ayuda a mantener las anotaciones de tipo coherentes con el código.

Fondo de mypy

La historia de mypy comenzó en el Reino Unido, en Cambridge, unos años antes de que me uniera a Dropbox. He estado involucrado, como parte de mi investigación doctoral, en la unificación de lenguajes estáticos y dinámicos. Me inspiré en un artículo sobre mecanografía incremental de Jeremy Siek y Walid Taha, y en el proyecto Typed Racket. Traté de encontrar formas de usar el mismo lenguaje de programación para varios proyectos, desde pequeños scripts hasta bases de código que constaban de muchos millones de líneas. Al mismo tiempo, quería asegurarme de que en un proyecto de cualquier escala, uno no tendría que hacer compromisos demasiado grandes. Una parte importante de todo esto fue la idea de pasar gradualmente de un proyecto de prototipo sin tipo a un producto terminado con tipo estático probado exhaustivamente. En estos días, estas ideas se dan por sentadas en gran medida, pero en 2010 era un problema que todavía se estaba explorando activamente.

Mi trabajo original en verificación de tipos no estaba dirigido a Python. En cambio, usé un pequeño lenguaje "casero" Álore. Aquí hay un ejemplo que le permitirá entender de qué estamos hablando (las anotaciones de tipo son opcionales aquí):

def Fib(n as Int) as Int
  if n <= 1
    return n
  else
    return Fib(n - 1) + Fib(n - 2)
  end
end

El uso de un idioma nativo simplificado es un enfoque común utilizado en la investigación científica. Esto es así, sobre todo porque le permite realizar experimentos rápidamente, y también porque lo que no tiene que ver con la investigación puede ignorarse fácilmente. Los lenguajes de programación del mundo real tienden a ser fenómenos a gran escala con implementaciones complejas, y esto ralentiza la experimentación. Sin embargo, cualquier resultado basado en un lenguaje simplificado parece un poco sospechoso, ya que al obtener estos resultados el investigador puede haber sacrificado consideraciones importantes para el uso práctico de los lenguajes.

Mi verificador de tipos para Alore parecía muy prometedor, pero quería probarlo experimentando con código real que, podría decirse, no estaba escrito en Alore. Afortunadamente para mí, el lenguaje Alore se basó en gran medida en las mismas ideas que Python. Fue bastante fácil rehacer el corrector de tipos para que pudiera funcionar con la sintaxis y la semántica de Python. Esto nos permitió probar la verificación de tipos en el código Python de fuente abierta. Además, escribí un transpiler para convertir código escrito en Alore a código Python y lo usé para traducir mi código de verificación de tipos. Ahora tenía un sistema de verificación de tipos escrito en Python que admitía un subconjunto de Python, ¡una especie de ese lenguaje! (Ciertas decisiones arquitectónicas que tenían sentido para Alore no eran adecuadas para Python, y esto todavía se nota en partes de la base de código de mypy).

De hecho, el lenguaje admitido por mi sistema de tipos no podía llamarse Python en este punto: era una variante de Python debido a algunas limitaciones de la sintaxis de anotación de tipos de Python 3.

Parecía una mezcla de Java y Python:

int fib(int n):
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Una de mis ideas en ese momento era usar anotaciones de tipo para mejorar el rendimiento al compilar este tipo de Python en C, o tal vez el código de bytes de JVM. Llegué a la etapa de escribir un prototipo de compilador, pero abandoné esta idea, ya que la verificación de tipos parecía bastante útil.

Terminé presentando mi proyecto en PyCon 2013 en Santa Clara. También hablé de esto con Guido van Rossum, el benévolo dictador de Python de por vida. Me convenció de abandonar mi propia sintaxis y apegarme a la sintaxis estándar de Python 3. Python 3 admite anotaciones de funciones, por lo que mi ejemplo podría reescribirse como se muestra a continuación, lo que daría como resultado un programa de Python normal:

def fib(n: int) -> int:
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Tuve que hacer algunos compromisos (en primer lugar, quiero señalar que inventé mi propia sintaxis por esta misma razón). En particular, Python 3.3, la versión más reciente del lenguaje en ese momento, no admitía anotaciones de variables. Discutí con Guido por correo electrónico varias posibilidades para el diseño sintáctico de tales anotaciones. Decidimos usar comentarios de tipo para las variables. Esto sirvió para el propósito previsto, pero fue algo engorroso (Python 3.6 nos dio una sintaxis más agradable):

products = []  # type: List[str]  # Eww

Los comentarios tipográficos también fueron útiles para admitir Python 2, que no tiene soporte integrado para anotaciones tipográficas:

f fib(n):
    # type: (int) -> int
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Resultó que estas (y otras) compensaciones realmente no importaban: los beneficios de la escritura estática significaron que los usuarios pronto se olvidaron de la sintaxis menos que ideal. Dado que no se usaron construcciones sintácticas especiales en el código de Python con verificación de tipo, las herramientas de Python existentes y los procesos de procesamiento de código continuaron funcionando con normalidad, lo que facilitó mucho el aprendizaje de la nueva herramienta para los desarrolladores.

Guido también me convenció de unirme a Dropbox después de completar mi tesis de grado. Aquí es donde comienza la parte más interesante de la historia de mypy.

To be continued ...

Estimados lectores! Si usa Python, cuéntenos sobre la escala de proyectos que desarrolla en este lenguaje.

El camino para verificar el tipo de 4 millones de líneas de código Python. Parte 1
El camino para verificar el tipo de 4 millones de líneas de código Python. Parte 1

Fuente: habr.com

Añadir un comentario