AttributeError: ‘Post’ object has no attribute ‘title’

Anotaciones de tipos en Python

Laski
Eryx
7 min readSep 8, 2020

--

Este artículo es una adaptación y actualización de esta charla que dí en la PyConAr 2018 mientras algunos hinchas de River le daban una cálida bienvenida al micro de Boca.

Si alguna vez escribiste algo en Python, seguro te cruzaste con algún

El mensaje puede parecer un poco confuso, pero refleja un problema típico de los lenguajes dinámicamente tipados: el intérprete no sabe de antemano el tipo de los objetos que usamos, con lo cual podríamos mandarles mensajes que no saben responder (en este caso perro.maullar()). En este artículo vamos a hablar sobre cómo podemos anotar nuestro código para que la IDE nos ayude a prevenir este tipo de problemas (y otras cosas incluso mejores).

El Counter de los jueves

Para divertirnos y socializar sin romper el aislamiento, en Eryx estamos jugando bastante al Counter-Strike (al 1.6, bien retro) al terminar la jornada laboral. Supongamos que queremos escribir un programa que nos permita recolectar datos de los partidos para armar una tabla de posiciones (spoiler: schouhy nos pasa el trapo). Y que une programadore amigue nos pasa un código para eso:

Por simplicidad omitimos el hecho de que un partido se divide en rondas, las bombas, los rehenes, etc.

Simple, ¿no? Una clase para los partidos, otra para los kills y un par de métodos para calcular el puntaje como lo calculaba el Counter original.

Más despacio cerebrito. ¿Cómo lo uso?

Como bien nota nuestre queride lectore imaginarie, si queremos usar el código sin mirar nada más nos va a faltar información. ¿De qué tipo son los players? Podrían ser un str, un int, un objeto más complejo. A decir verdad, ni siquiera tenemos seguridad de que killer y victim sean del mismo tipo. Y la IDE tampoco lo sabe: si escribimos player. el autocomplete va a dar sugerencias genéricas que no van a servir de mucho.

Con ustedes… un autocomplete que molesta más de lo que ayuda.

Bueno, ya fue, ¿no? Que sean del tipo que nosotros queramos.

Podríamos elegir cualquiera, es verdad. Pero si este código es parte de algo mayor, nos estamos arriesgando a romper el resto del programa.

La mejor manera que tenemos para poder responder esta pregunta es ir a ver el código que usa estas clases, idealmente los tests. Por suerte, como todes hacemos TDD (¿no?), nos pasaron también un test:

Chou siempre gana

¡Genial! Ahora sabemos que los players son representados por strings. Aunque tuvimos que ir a mirar otro código para enterarnos. Y la IDE, aunque quizás podría usar esta información, no parece estar haciéndolo muy bien.

Extendiendo el modelo

Supongamos que ahora queremos saber también la IP desde donde están entrando los jugadores. Ya no nos alcanza con usar strings. Vamos a agregarle a nuestro modelo una abstracción para les jugadores:

Y, como no queremos que el puntaje dependa de la IP, empezar a indexar el puntaje solo por el nick:

Por suerte tenemos tests que nos avisan que este cambio probablemente rompa el código que usa a estos objetos:

porque, claro, un string no sabe responder al mensaje nick.

¿Podría la IDE haberme avisado incluso antes de que yo ejecute los tests? Aún mejor, ¿podría haberme avisado qué otras partes tendría que cambiar para que todo funcione?

Anotando por la vida

Uno de los cambios que introdujo Python 3 fue una sintaxis para “anotar”, en la declaración de un método, los tipos de los parámetros que el método espera y el tipo del objeto que va a devolver. Veamos cómo:

Vemos varias cosas nuevas. De a una:

  • Los tipos básicos (int, str) se llaman igual que sus constructores, pero los tipos compuestos (list, dict) hace falta escribirlos con mayúscula e importarlos de typing.
  • Los tipos internos de un tipo compuesto (los elementos de una lista, las claves y valores de un diccionario, etc.) van entre [corchetes].
  • El tipo de un parámetro se anota con : . El tipo de retorno de un método, con una simpática flechita -> .
  • Si un método no devuelve nada (o sea, devuelve None), se anota -> None.
  • Los métodos __init__ nunca devuelven nada, con lo cual no hace falta explicitarlo (aunque tampoco está prohibido).
  • La mayoría de las veces no hace falta anotar explícitamente el tipo de las variables, porque se puede inferir a partir de lo que hay a la derecha del =. Sin embargo, hay casos donde eso no es suficiente, por ejemplo cuando creamos una colección vacía. Para estos casos, a partir de Python3.6 podemos anotar la variable también, por ejemplo en self.events: List[KillEvent] = [].
  • En líneas generales se siguen respetando las reglas de espaciado de PEP8, aunque vale la pena volver a leerlo porque hay un par de casos interesantes.

Lo bueno y lo no tan bueno

Gracias a esos cambios, mi IDE ya empieza a ver algunos errores:

PyCharm avisando que hay algo raro

Ahora que tiene más información, el autocomplete ofrece mejores opciones:

Con ustedes… un autocomplete útil.

Además, el código se hace más autoexplicativo: podemos ver rápidamente el tipo de los objetos sin ponernos a buscar en dónde se definieron por primera vez (lo cual podría ser mucho más arriba en el stack de llamados).

Y quizás lo más importante de todo: tanto las herramientas de find usages/go to definition (el famoso ctrl+clic) como las de refactor/renaming automático hacen mucho mejor su trabajo cuando pueden distinguir el tipo de los objetos con los que trabajan.

Todo muy lindo, pero ¿dónde está la trampa? Tiene que haber alguna.

Para empezar, escribir y mantener las anotaciones toma trabajo. Más abajo hablaremos un poco sobre cómo minimizarlo.

También hay quienes opinan que el código se vuelve más difícil de leer. En mi experiencia uno se acostumbra rápido y, como termina ayudando más veces que las que no, lo terminás agradeciendo. Pero no me sorprendería ver en el futuro una herramienta que permita “ocultar” las anotaciones de tipos.

Y, hablando de eso, en runtime las anotaciones se ignoran, por lo que si te estás preguntando por la performance, el impacto es prácticamente nulo.

Chequeo de tipos

Uno de los motivos más populares de agregar anotaciones de tipos a un lenguaje es mejorar y facilitar el uso de herramientas de chequeo de tipos. A uno le gustaría creer que con una buena cobertura de tests esto ya debería estar bastante bien cubierto, pero eso podría ser falso. Además, validar las anotaciones es la única manera de garantizar que no queden desactualizadas (recordemos que el intérprete las ignora).

En particular para Python existe una herramienta llamada mypy que analiza el código estáticamente (es decir sin ejecutarlo) en busca de errores de tipos. En el código que pegué arriba, por ejemplo, mypy descubrió algo que ni PyCharm ni yo detectamos:

Cuando cambiamos los jugadores de str a Player nos olvidamos de modificar el método score_for .

Algo bueno de mypy es que “entiende” que la mayoría de los mortales nunca va a tener absolutamente todo el código anotado, ya sea porque importamos bibliotecas ajenas, porque es un proceso largo o simplemente porque no nos interesa. mypy valida solamente el código que está anotado e ignora el que no. Eso nos permite ir anotando progresivamente, y agregar desde el principio mypy a nuestro pipeline de CI/CD (al lado de nuestra suite de tests) para que nos alerte de los posibles errores de tipos antes de un deploy.

Automaticemos todo

Seamos sinceros: anotar los tipos de un código que ya está andando parece una tarea mecánica y aburrida. ¿Y qué nos gusta hacer a les programadores con las tareas mecánicas y aburridas? Adivinaste (o leíste el subtítulo de arriba).

Nobleza obliga, no somos muy originales: este paper de 1978 (¡hace más de 40 años!) ya hablaba sobre cómo hacer inferencia de tipos en lenguajes dinámicamente tipados.

Volviendo a Python, tanto Dropbox como Instagram hicieron bibliotecas para anotar automáticamente su código: pyannotate por un lado y MonkeyType por otro. La idea es la misma: ejecutar los tests recordando qué tipos toman las diferentes variables y luego anotarlas. Como dicen en el README de MonkeyType, esto puede dar resultados demasiado específicos, pero es un buen punto de partida que debería ahorrar bastante trabajo.

Resumen de los links que deberías guardarte

Y dos herramientas que todavía no probé pero que se ven prometedoras:

  • Pyre (una herramienta de Facebook tipo mypy que agrega también análisis de seguridad).
  • MonkeyType (pyannotate funciona pero luce medio abandonada).

--

--