Macros para un pitonista. Informe Yandex

¿Cómo puedo extender la sintaxis de Python y agregarle las características necesarias? El verano pasado en PyCon intenté descifrar este tema. En el informe puede averiguar cómo se organizan las bibliotecas de patrones pytest, macropy, y cómo logran resultados tan interesantes. Al final hay un ejemplo de generación de código utilizando macros en HyLang, un lenguaje similar a Lisp que se ejecuta sobre Python.


- Hola chicos. En primer lugar, quiero agradecer a los organizadores de PyCon. Soy desarrollador en Yandex. El informe no será sobre el trabajo en absoluto, sino sobre cosas experimentales. Tal vez lo lleven a uno de ustedes a la idea de que en Python pueden hacer cosas geniales que ni siquiera sabían antes, que no pensaban en esta dirección.

Un poco para aquellos que no saben lo que son las macros: esta es una forma de generación de código cuando una expresión en el lenguaje se expande en código más complejo. ¿Cuáles son las golosinas para ti? Para usted, el registro macro es conciso, expresa algo de abstracción, pero hace mucho trabajo bajo el capó para usted y no necesita escribir todo este código con las manos.

pytest


Lo más probable es que te hayas encontrado con un marco de prueba de pytest, muchos aquí seguramente lo usan. No sé si alguna vez lo has notado, pero bajo el capó también hace algo de magia.



Por ejemplo, tienes una prueba tan simple. Si lo ejecuta sin pytest, arrojará un AssertionError simplemente.



Desafortunadamente, mi ejemplo es un poco degenerado, y aquí es inmediatamente obvio que Len está tomado de una lista de tres elementos. Pero si se llamara a alguna función, entonces nunca hubiera sabido de un AssertionError que la función regresó. Ella devolvió algo que no es igual a cien.



Sin embargo, si se ejecuta bajo pytest, mostrará información de depuración adicional. ¿Cómo lo hace adentro?



Esta magia funciona de manera muy simple. Pytest crea su propio gancho especial que se activa cuando se carga el módulo con la prueba. Después de eso, pytest analiza de forma independiente este archivo de Python y, como resultado del análisis, se obtiene su representación intermedia, que se denomina árbol AST. El árbol AST es un concepto básico que le permite cambiar el código de Python sobre la marcha.

Después de recibir dicho árbol, pytest le impone una transformación que busca todas las expresiones llamadas aserciones. Los cambia de cierta manera, compila el nuevo árbol AST resultante y obtiene un módulo con pruebas, que luego se ejecuta en una máquina virtual Python normal.



Así es como se ve el árbol AST original no convertido a pytest. El área roja resaltada es nuestra Afirmación. Si observa detenidamente, verá sus partes izquierda y derecha, la lista misma.

Cuando pytest convierte esto y genera un nuevo año, el árbol comienza a verse así.



Hay alrededor de un centenar de líneas de código que Pytest generó para usted.



Si convierte este árbol AST de nuevo a Python, se verá más o menos así. Las áreas resaltadas en rojo aquí son donde pytest calcula las partes izquierda y derecha de la expresión, genera un mensaje de error y genera un error de aserción si algo salió mal con este mensaje de error.

La coincidencia de patrones


¿Qué más puedes hacer con tal cosa? Puedes convertir cualquier código de Python. Y hay una biblioteca maravillosa que encontré por accidente en PyPI, es interesante excavar allí. Ella combina patrones.



Quizás este código le sea familiar a alguien. Él considera factorial recursivamente. Veamos cómo se puede grabar usando la coincidencia de patrones.



Para hacer esto, simplemente cuelgue el decorador en la función. Tenga en cuenta: dentro del cuerpo, la función ya funciona de manera diferente. Cada uno de estos ifs es una regla para la coincidencia de patrones, que analiza la expresión que se ingresa a la función y de alguna manera la transforma. Además, ni siquiera hay retornos explícitos del resultado. Debido a que la biblioteca de patrones, cuando transforma el cuerpo de la función, en primer lugar, comprueba que contiene solo si, y en segundo lugar, agrega retornos implícitos del resultado, cambiando así la semántica del lenguaje. Es decir, ella hace un nuevo DSL, que funciona un poco diferente. Y gracias a esto, puedes escribir algunas cosas declarativamente.


La función anterior es como si estuviera escrita en tres líneas.





Y el resto de las líneas agregan funcionalidad adicional que permite, por ejemplo, leer factorial de una lista de valores o pasarlo a través de una función arbitraria.

¿Cómo escribir conversiones usted mismo? macropy!


Ahora probablemente se esté preguntando, pero ¿cómo puede aplicarlo usted mismo? Debido a que es tedioso hacer, como pytest: analizar manualmente los archivos, busque el código que necesita ser convertido. En pytest, esto se realiza mediante un módulo separado para mil o más líneas.

Para no hacer esto por nuestra cuenta, algunos tipos inteligentes ya han creado un módulo para nosotros llamado macropy.

Esta versión del módulo es tanto para el segundo Python como para el tercero. Lo escribieron en el tiempo de la segunda Python. Luego, los chicos hicieron una broma para descubrir qué se puede hacer con Python, y la biblioteca incluye varios ejemplos. Echemos un vistazo a ellos, te darán una idea de lo que puedes hacer con esta técnica. Lo primero que describieron en el tutorial es una macro que implementa cadenas de formato para el segundo Python, como en el tercero.



La expresión resaltada en rojo es solo la sintaxis de la llamada macro. La letra S es el nombre de la macro, y luego entre corchetes es la expresión que convierte. Como resultado, las variables se sustituyen aquí. Esto funciona en el segundo Python, pero el tercero ya no es necesario en dicha macro. Así, por ejemplo, puede crear su propia macro, que implementa una semántica más compleja y hace cosas más divertidas que las cadenas de formato estándar.



Cuando una macro se expande, y esto sucede al momento de cargar el módulo, simplemente se convierte a ese código. Los marcadores de posición se insertan en la cadena de formato y se le aplica el procedimiento de sustitución. Además Python ya de una manera estándar compila todo esto. En tiempo de ejecución, no se producen expansiones macro. Todos ocurren cuando se carga el módulo. Por lo tanto, en tal caso, incluso puede hacer optimizaciones o cálculos que ocurrirán al momento de cargar el módulo y generar un código de bytes más óptimo.



El segundo ejemplo también es interesante. Esta es una notación abreviada para escribir lambdas. La macro f toma una serie de argumentos y devuelve una función en su lugar. Cada expresión que comienza con el nombre de macro "f", paréntesis, y luego absolutamente cualquier expresión se convierte en lambda.



En mi opinión, esto también es genial, especialmente para aquellos a quienes les gusta desarrollar y escribir código en un estilo funcional y usar MapReduce.


Aquí hay otro ejemplo familiar. Esta función considera factorial, el código se resalta en rojo. ¿Qué pasará cuando la llamen?



Lanzará un error en Python, porque se ejecutará en el límite de la pila y habrá un RecursionError tan feo.



¿Cómo se puede arreglar esto? Usando macropy, solucionar el problema es muy simple.



Cuelgas el decorador, toma el cuerpo de la función y lo transforma de una manera mágica. No necesita cambiar nada en la función en sí, macropy hará todo por usted.



Y la función volverá a ser un resultado bastante normal, yendo lejos al subsuelo.


¿Cómo lo hace macropy?



Reemplaza todas las llamadas a la función en sí con un objeto especial TailCall, que luego es llamado en un bucle por el decorador de TCO.



El circuito se parece a esto. El decorador en el bucle llama a la función hasta que devuelva algún resultado normal en lugar de TailCall. Y si ella regresó, entonces lo devuelve. Y eso es todo. ¡Estas cosas geniales se pueden hacer con macros!

Macropy también incluye otros ejemplos. Espero que aquellos que sienten curiosidad por ti vayan a verlos por su cuenta. Digamos que hay cosas útiles para la depuración.



Te contaré sobre otra cosa genial. Un ejemplo es esta macro de consulta. ¿Qué está haciendo? Dentro de él, escribe código Python regular, que luego puede usar como resultado regular de ejecutar esta expresión. Pero por dentro, macropy transforma este código y lo convierte en el código del lenguaje de consulta Alchemy SQL.



Lo reescribe para ti, hace esta terrible expresión. Se puede reescribir a mano, luego será más corto. Lo hice.



Aquí está la expresión original. Después de expandir la macro, toma algo como esto.



Quizás alguien esté interesado en escribir código más similar a Python, y no obligar a sus desarrolladores a escribir consultas sobre DSL SQL Alchemy.

De la misma manera, puede generar cualquier cosa desde Python - SQL puro, JavaScript - y guardarlo en algún lugar al lado del archivo, y luego usarlo en la interfaz.



Ahora veamos cómo hacer tu propia macro. Con macropy, es muy simple.

Una macro es una función que toma un árbol AST en la entrada y, de alguna manera transformándolo, devuelve uno nuevo. Aquí hay un ejemplo de macro que agrega una descripción a la llamada de aserción que contiene la expresión de origen para que podamos entender por qué ocurrió el error AssertionError.

Aquí, la función interna replace_assert es auxiliar. Ella hace un descenso recursivo en un árbol para ti. Dentro de replace_assert, se pasa el elemento de subárbol.



Debido a esto, puede verificar su tipo y? Si se trata de una llamada de afirmación, haga algo con ella. Aquí daré un ejemplo sintético simple que toma la parte izquierda, la parte derecha, genera un mensaje de error de ellos y escribe todo en el atributo msg. Este es el mensaje que deberá devolverse.







Cuando lo usa, adjunta dicha macro a un bloque de código usando el administrador de contexto with, y todo el código que ingresa al administrador de contexto pasa por esta transformación. A continuación se ve que nuestro mensaje de error se agregó a AssertionError, que formamos a partir de la expresión len ([1, 2, 3]).



Sin embargo, este método tiene una limitación que me pone triste personalmente. Intenté como experimento hacer nuevos diseños que funcionen en el idioma. Por ejemplo, a algunas personas les gusta el interruptor o las construcciones condicionales, a menos que. Pero desafortunadamente, esto no es posible: macropy y cualquier otra herramienta que funcione con el árbol AST se usa cuando el código fuente ya se lee y se divide en tokens. El código es leído por el analizador Python, cuya gramática está arreglada en el intérprete. Para cambiarlo, necesita recompilar Python. Por supuesto, puede hacer esto, pero ya será una bifurcación de Python, y no una biblioteca que se pueda diseñar en PyPI. Por lo tanto, es imposible hacer tales construcciones usando macropy.

HyLang


Afortunadamente, durante mi larga vida escribí no solo en Python y me interesaron varios otros lenguajes alternativos. Hay una sintaxis que a muchos no les gusta, pero más simple y flexible. Estas son expresiones s.

Afortunadamente para nosotros, hay un complemento de Python llamado HyLang. Esto recuerda un poco a Clojure, solo Clojure se ejecuta sobre JVM y HyLang se ejecuta sobre Python Virtual Machine. Es decir, le proporciona una nueva sintaxis para escribir código. Pero al mismo tiempo, todo el código que escriba será totalmente compatible con las bibliotecas de Python existentes, y puede usarse desde las bibliotecas de Python.



Se ve algo como esto.



La parte de la izquierda escrita en Python, a la derecha, en HyLang. Y desde abajo para ambos hay un bytecode, que es el resultado. Probablemente notó que es exactamente lo mismo, solo cambia la sintaxis. Expresiones s de HyLang, que a muchos no les gusta. Los opositores de los "corchetes" no entienden que tal sintaxis le da al lenguaje un poder tremendo porque le da uniformidad a las construcciones del lenguaje. Y la uniformidad le permite usar macros para implementar cualquier diseño.

Esto se logra debido al hecho de que dentro de cada expresión el primer elemento es siempre algún tipo de acción. Y luego sus argumentos van.

Y todo el código está compuesto de expresiones anidadas que son fáciles de convertir y abrir macros allí. Debido a esto, absolutamente cualquier construcción se puede hacer en HyLang, nueva, de ninguna manera indistinguible de las características del lenguaje estándar en el código.



Veamos cómo funciona una macro simple en HyLang. Para hacer lo mismo que hicimos con Assert usando macropy, solo necesita este código.

Nuestra macro HyLang recibe entrada, que es código. Además, una macro puede usar fácilmente cualquier parte de este código para crear un nuevo código. La principal diferencia entre macros y funciones: las expresiones son entradas, no valores. Si llamamos a nuestra macro como (es (= 1 2)), recibirá una expresión (= 1 2) en lugar de False.



Entonces podemos generar un mensaje de error de que algo salió mal.



Y luego solo devuelve el nuevo código. Esta sintaxis de tilde y tilde significa algo como lo siguiente. La cita posterior dice: tome esta expresión como es y devuélvala como es. Y la tilde dice: sustituye el valor de la variable aquí.



Por lo tanto, cuando escribimos esto, la macro al expandirse nos devolverá una nueva expresión, que de este modo se afirmará con un mensaje de error adicional.

HyLang es algo genial. Es cierto, mientras no lo usemos. Quizás nunca lo hagamos. Todos estos artículos son experimentales. Quiero que te vayas de aquí con la sensación de que en Python puedes hacer algunas cosas que ni siquiera habías pensado antes. Y quizás algunos de ellos encuentren una aplicación práctica en su trabajo continuo.

Eso es todo para mí. Puedes ver los enlaces:

  • Patrones ,
  • MacroPy ,
  • HyLang ,
  • El libro OnLisp : para un estudio avanzado de las capacidades de las macros. Esto es para aquellos especialmente interesados. Es cierto que el libro no está completamente basado en Python, sino en Common Lisp. Pero para un estudio más profundo, esto será incluso interesante.

All Articles