REPL inútil. Informe Yandex

REPL (bucle read-eval-print) es inútil en Python, incluso si es IPython mágico. Hoy ofreceré una de las posibles soluciones a este problema. En primer lugar, el informe y mi extensión TheREPL serán útiles para aquellos que estén interesados ​​en un desarrollo más rápido y más eficiente, así como para aquellos que escriben sistemas con estado.


- Mi nombre es Alexander, trabajo como programador en Yandex. Estamos escribiendo en mi equipo en Python, aún no hemos cambiado a Go. Pero en mi tiempo libre, curiosamente, también programo y lo hago en un lenguaje muy dinámico: Common Lisp. Es quizás incluso más dinámico que Python. Su peculiaridad radica en el hecho de que el proceso de desarrollo en sí está organizado de manera algo diferente. Es más interactivo e iterativo, porque en REPL en Lisp puedes hacer todo: crear módulos nuevos y eliminar viejos, agregar métodos, clases y eliminarlos, redefinir clases, etc.



En Python, esto es aún más difícil. Cuenta con IPython. Por supuesto, IPython mejora REPL de alguna manera, agrega autocompletado y permite usar diferentes extensiones. Pero para el desarrollo iterativo, no encaja muy bien. En él puedes descargar el código, probarlo un poco y listo. Y a veces quiere más interactividad para que realmente pueda usar este REPL en el desarrollo, cambiar entre módulos, cambiar funciones y clases dentro de ellos.

Me sucede a mí: ejecutas, por ejemplo, IPython REPL en el entorno de producción y comienzas a ejecutar algunos comandos allí, investigas algo, y luego resulta que hay un error en el módulo y quieres solucionarlo rápidamente. Pero esto no funciona, porque necesita construir una nueva imagen de Docker, ponerla en producción, volver a este REPL, alcanzar el estado deseado allí nuevamente, iniciar todo lo que cayó nuevamente. E idealmente, tendría que arreglar la función, ejecutarla inmediatamente y obtener el resultado al instante.

¿Qué se puede hacer con esto? ¿Cómo puedo recargar el código en IPython? Intenté usar la recarga automática y no me gustó por varias razones. En primer lugar, cuando se reinicia el módulo, pierde el estado que estaba en las variables globales dentro de este módulo. Y puede haber un valor en caché con los resultados de algunas funciones. O podría, por ejemplo, cargar datos a través de la red allí, para luego poder trabajar con ellos más rápido. Es decir, la recarga automática pierde estado.

Por lo tanto, como experimento, hice mi extensión simple para IPython y la llamé TheREPL.

Llegué a usted con este informe como una idea de lo que se puede hacer con REPL en Python. Y realmente espero que te guste esta idea, la llevarás a la mente y seguirás creando cosas que harán que Python sea aún más eficiente y conveniente.

¿Qué es TheREPL? Esta es la extensión que descarga, después de lo cual aparece un concepto como el espacio de nombres en IPython, y puede tomar y cambiar a cualquier módulo de Python, ver qué variables, funciones, etc. Y lo más importante, puede escribir directamente def, el nombre de la función, redefinir la función o clase, y cambiará en todos los módulos donde se importó. Pero al mismo tiempo, el módulo en sí no se reinicia, por lo que se guarda el estado. Además, TheREPL le permite evitar algunos artefactos más que se encuentran en la recarga automática y que ahora veremos.



Entonces, en la recarga automática, la actualización del código solo ocurre cuando se guarda el archivo. Pero al mismo tiempo, debe ingresar algo en el REPL, y solo entonces la recarga automática detectará estos cambios. Este es el problema número 1. Es decir, si tiene algún tipo de proceso en segundo plano en un hilo separado (por ejemplo, el servidor se está ejecutando), no puede simplemente tomar y corregir el código. La recarga automática no aplicará estos cambios hasta que ingrese algo en IPython REPL.

En el caso de mi extensión, presionas el acceso directo directamente en el editor, y la función que está debajo del cursor se aplica inmediatamente y comienza a funcionar. Es decir, usando TheREPL, puede cambiar el código de forma más granular. También puede escribir def en IPython.



Cambiar entre módulos, como dije, la recarga automática no es compatible de ninguna manera. Solo puede encontrar el archivo en el sistema de archivos, cambiarlo y esperar que la recarga automática resuelva todo lo que haya allí.



Más lejos. La recarga automática pierde variables globales, TheREPL guarda y le permite continuar investigando el funcionamiento de su aplicación, cambiar su código interno y así desarrollarlo rápidamente.



La recarga automática todavía tiene esta característica. Con mucha astucia aplica cambios al módulo que se recarga. En particular, hace un truco muy interesante allí. Si la función en este módulo se ha actualizado, para cambiarla donde se importó, utiliza el recolector de basura para encontrarla y todas estas instancias de funciones y cambiar el código dentro de ellas. Además, veremos ejemplos de cómo sucede esto. Debido a esto, el código de función cambia, incluso si entra en el cierre.

¿Sabes qué es un cierre? Esto es algo muy útil. Los desarrolladores de JavaScript usan esto todo el tiempo. Usted, muy probablemente, simplemente nunca prestó atención. Pero dado que la carga automática hace lo que describí anteriormente, es posible que se encuentre en una situación en la que el código anterior usa un código nuevo que puede funcionar de manera diferente. Por ejemplo, una función puede devolver no un valor, sino dos, tupla en lugar de cadena, etc. El código anterior se romperá en esto.

TheREPL no hace algo tan complicado específicamente para garantizar que todo sea más consistente. Es decir, cambia la función o clase en el módulo en el que se define. Encuentra esta clase en todos los demás módulos y la cambia allí también. Después de eso, todo funciona de una manera nueva.



¿Cómo funciona la sustitución de la función de autocarga? Tenemos dos funciones, una y dos. Cada función tiene un conjunto de atributos: documentación, código, argumentos, etc. Aquí en la diapositiva hay un ejemplo de reemplazo de los atributos en los que se almacena el código de bytes.

Después de que la carga automática lo cambie, la función llamada comienza a funcionar de manera diferente. Pero este es un ejemplo sintético que acabo de reproducir con mis manos para que entiendan lo que está sucediendo. La función se llama de una manera, pero el código allí es realmente diferente. Y si desmonta, también muestra que devuelve un deuce. ¿A qué conduce esto?



Aquí hay un ejemplo de cierre. En la segunda línea, creamos un cierre en el que capturamos la función foo. El cierre en sí mismo espera que esta función que pasamos devuelve una línea, la codifica en utf-8 y todo funciona.



Pero suponga que cambia el módulo en el que se define foo, y la recarga automática recoge el cambio. Y lo cambia para que no devuelva una cadena, sino un número. Entonces el cierre ya funcionará incorrectamente, porque la función en él ha cambiado por dentro, pero el cierre no espera esto, no ha cambiado. Y tales problemas con la recarga automática pueden "disparar" en lugares inesperados.



¿Cómo se autocargan las clases de actualización? Muy simple. Actualiza todos los métodos de la clase de la misma manera que las funciones, y también actualiza el atributo __class__ para todas las instancias, de modo que la resolución de los métodos (determinar qué método debe llamarse) comienza a funcionar de una manera nueva.

Todo es un poco más complicado en TheREPL, porque cuando actualiza _class_, puede resultar que tenga algunos descendientes, clases secundarias, que también deben actualizarse, porque algo ha cambiado en la lista de clases base.

Para resolver este problema, puede reconstruir la clase. Pero primero veamos qué sucede con la recarga automática cuando recarga un módulo.



Aquí hay un buen ejemplo. Hay dos módulos: a y b. En el módulo a, se define una clase principal, en el módulo b una clase secundaria, y creamos una instancia de la clase secundaria. Y la línea 10 muestra que sí, esta es una instancia de la clase Foo, el padre.



A continuación, simplemente tomamos y cambiamos el módulo a. Por ejemplo, agregue documentación a la clase Foo. Luego, la recarga automática recoge estos cambios. ¿Qué crees que en este caso volverá de Bar?



Y devuelve falso, porque la recarga automática ha cambiado la clase Foo, y ahora es una clase completamente diferente, no de la que se hereda la clase Bar.



Y una sorpresa! En los dos módulos ayb, la clase Foo es una clase diferente y Bar hereda de uno de ellos. Debido a tales jambas, es muy difícil predecir cómo funcionará su código después de que la recarga automática solucione algo en él.



Algo como esto, actualiza las clases. Voy a comentar sobre la foto. Inicialmente, la clase Foo se importa al módulo b, por lo que permanece allí. Al reemplazar la recarga automática, este módulo se reubica, y aparece una nueva clase allí, y en el módulo b no se actualiza.



TheREPL hace un poco diferente. Inyecta una clase modificada en cada módulo donde fue importado. Por lo tanto, todo funciona correctamente allí. Además, si hubiera objetos en la clase, se conservarán.



Y así es como TheREPL resuelve el problema con las clases secundarias. Es decir, cuando la clase padre ha cambiado, define la lista de clases base a través del atributo mágico mro (orden de resolución del método). Este atributo contiene una lista de clases en el orden en que desea buscar métodos o atributos en ellos. Y cada vez que llame al método get_name en su objeto, por ejemplo, Python lo comprobará primero en la clase Bar, luego en la clase Foo, luego en la clase de objeto, si no lo encuentra. Actúa de acuerdo con el procedimiento de orden de resolución del método.

TheREPL usa este chip. Toma una lista de clases base, cambia allí la clase que acaba de cambiar a una nueva. Crea un nuevo tipo secundario, este es el segundo paso. Con la función type, puedes crear clases. Si nunca lo ha usado, pruébelo, es divertido.

Simplemente diga el nombre de la clase, diga cuál es su clase base. En el caso más simple, por ejemplo, objeto. Y - un diccionario con métodos y atributos de clase. Todo, tiene una nueva clase que puede instanciar, como de costumbre. TheREPL aprovecha este chip. Genera una clase secundaria y le cambia punteros en todos los objetos de la antigua clase Bar.

Todavía tengo una demostración, echemos un vistazo a cómo funciona. Primero, veamos una cosa tan simple.

Primera demo

Dije que puedes cambiar el código dentro del módulo. Supongamos que tenemos un servidor. Lo ejecutaré ahora. En algún momento, encontramos que por alguna razón crea directorios temporales. O comenzó a crear, pero antes de eso no creó. Luego podemos conectarnos a este servidor y, suponiendo que probablemente crea estos directorios usando la función mkdtemp del módulo de archivos, puede ir directamente a este módulo de Python.

Ver - en la esquina el nombre del módulo actual ha cambiado. Ahora dice archivo temporal. Y puedo ver qué características hay. Los vemos y, lo que es más importante, podemos redefinirlos. He preparado un contenedor especial que le permite decorar cualquier función para que con todas sus llamadas pueda ver el rastro desde donde se llama. Ahora los importaremos y aplicaremos.

Es decir, envuelvo la función estándar de Python, sin siquiera tener acceso al código fuente de este módulo. Puedo tomarlo y envolverlo. Y en la próxima salida, veremos Traceback y encontraremos desde dónde se llama.

Del mismo modo, estos cambios se pueden revertir para que no nos envíe spam. Es decir, vemos que este servidor dentro del trabajador en la octava línea llama a mkdtemp y continúa produciendo directorios temporales para nosotros, abarrotando el sistema de archivos. Esta es una aplicación.

Veamos otro ejemplo de por qué la recarga automática a veces no funciona muy bien. Tengo preparado un bot de telegramas:

Segunda demo

Ahora activamos la recarga automática y vemos cómo nos ayuda. Eso es todo, ahora puedes iniciar el bot y hablar con él. Para que pueda ver mejor, comenzaremos un diálogo con él. Conozca el bot. Entonces. Hay algún tipo de error. Se concibió un error completamente diferente, y decidí hacer cambios en el último momento. Pero no importa. Ahora lo arreglaremos, la recarga automática nos ayudará con esto.

Estamos cambiando al bot. Y ahora comentaré temporalmente sobre esto, si es así. Guardo el archivo La recarga automática, en teoría, tuvo que atrapar estos cambios. Inicie el bot nuevamente. El bot me reconoció. Hablemos con el.

Otro error. Ella ya está concebida. Vamos a arreglarlo. Dejaré el bot, funcionará en segundo plano, cambiaré al editor y en el editor encontraremos este error. Es solo un error tipográfico, y olvidé que mi variable se llama user_name. Guardé el archivo. se suponía que la recarga automática la atraparía, y ahora lo veremos.

Pero la recarga automática, como ya mencioné, no sabe nada sobre el hecho de que el archivo ha cambiado hasta que ingresas algo en él. Con un proceso tan largo ... Necesita ser interrumpido, reiniciado. Hecho. Vuelve a nuestro bot, escríbele. Bueno, ya ves, el bot olvidó que mi nombre es Sasha. ¿Por qué? autoreload lo recreó nuevamente porque recarga todo el módulo por completo. Y necesito volver a escribir en el bot para restaurar su estado.

Y si está depurando algún tipo de error que ocurre en cierto estado, entonces el estado no puede perderse, porque de lo contrario pasará mucho tiempo nuevamente para lograr este estado. TheREPL ayuda en esos casos.

Veamos cómo se actualizará el bot en caso de usar TheREPL. Para la pureza del experimento, reiniciaré IPython y lo repetiremos nuevamente.

Y ahora descargo TheREPL. Inmediatamente comienza a escuchar en un puerto específico para que pueda enviar un código dentro de él. Por cierto, esto se puede hacer incluso si IPython se ejecuta en algún lugar del servidor y el editor se ejecuta localmente, lo que también puede ayudarlo en algunos casos.

Importamos el bot, lo iniciamos y volvemos a escribir. Está claro aquí: reiniciamos Python, por lo que no recuerda quién soy. Verifique que haya un error adentro. Si, hay un error. Bueno, hagámoslo.

Vuelvo al editor, corrijo el error. Ni siquiera tenemos que guardar el archivo, presiono Ctrl-C, Ctrl-C, este es un acceso directo mediante el cual Emacs toma la descripción actual de la función que está justo debajo del cursor y la envía al proceso de Python al que está conectado. Eso es todo, ahora podemos pasar y verificar cómo nuestro bot responde a mis mensajes allí. Ahora, recuerda que yo soy Sasha, y honestamente responde que no sabe cómo.

Intentemos agregar directamente una nueva funcionalidad allí. Para hacer esto, regrese al editor. Por ejemplo, agregue el comando de ayuda. Por ahora, que responda que no sabe nada de ayuda. Nuevamente, presione Ctrl-C, Ctrl-C, se aplica el código. Vamos al bot. Vea si él entiende este comando. Sí, el equipo ha aplicado.

Por cierto, él todavía tiene tal cosa, ahora veremos cómo cambiará la clase. Tiene un comando de estado, un comando de depuración especial para ver el estado del bot. Entonces, algunos Oleg se conectaron. Interesante.

Cuando el bot ejecuta este comando, llama a responder para ver la representación del bot. Podemos ir y corregir, por ejemplo, esta respuesta con otra cosa. Por ejemplo, haga que se ingresen solo los nombres. Puedes hacerlo Volvemos a nuestro mensajero, nuevamente ejecutamos estado. Y eso es todo. Ahora la respuesta funciona de una manera nueva, pero el objeto es el mismo, ha conservado su estado, ya que nos recuerda a todos: Oleg, Sasha, kek y "DROP TABLE Users, Alex".

Por lo tanto, puede escribir y depurar código directamente sobre la marcha, sin cambiar a este ciclo, cuando necesite recolectar un paquete, enróllelo en algún lugar. Puede probar rápidamente algo, cambiar todo lo que necesita, y solo entonces todos estos cambios deben empaquetarse correctamente y desplegarse.

Naturalmente, no debe hacer esto en la producción real, porque con este enfoque, qué tipo de problema puede ser. Puede olvidar que el código que acaba de iniciar en el servidor debe guardarse y luego implementarse como debería. Este enfoque requiere disciplina. Pero en el proceso de desarrollo y depuración de algún tipo de prueba, esto es simplemente una gran cosa.

Asegúrese de hacer un complemento para PyCharm. Si hay un voluntario que me ayudará con Kotlin y el complemento PyCharm, me complacerá hablar. Escríbeme en el correo o telegrama .

* * *

Conectarpara el desarrollo de TheREPL. Hay muchas más fichas que se te ocurran. Por ejemplo, puede encontrar una manera de actualizar las instancias de clase cuando se actualizan, agregar nuevos atributos allí o actualizar su estado de alguna manera. Del mismo modo, actualizaremos la base de datos. Ahora esto no es.

Puede crear un código de recarga en caliente para la producción, de modo que cuando reciba nuevos cambios, no tenga que reiniciar el servidor. Puedes llegar a mucho más. Esto es solo una idea, y quiero que lo saques de aquí. Debemos ajustar todo por nosotros mismos y hacerlo conveniente. Eso es todo para mi.

All Articles