Macros para um pythonist. Relatório Yandex

Como posso estender a sintaxe do Python e adicionar os recursos necessários? No verão passado, na PyCon, tentei entender esse tópico. No relatório, você pode descobrir como as bibliotecas pytest, macropy e padrões são organizadas e como elas alcançam resultados tão interessantes. No final, há um exemplo de geração de código usando macros no HyLang, uma linguagem semelhante ao Lisp executando no Python.


- Oi pessoal. Antes de tudo, quero agradecer aos organizadores da PyCon. Sou desenvolvedor da Yandex. O relatório não será sobre trabalho, mas sobre coisas experimentais. Talvez eles levem um de vocês à idéia de que, em Python, você pode fazer coisas legais que você nem sabia antes, que não pensavam nessa direção.

Um pouco para quem não está ciente do que são macros: esse é um método de geração de código quando alguma expressão na linguagem é expandida para um código mais complexo. Quais são os presentes para você? Para você, o registro macro é conciso, expressa alguma abstração, mas faz muito trabalho para você e você não precisa escrever todo esse código com as mãos.

pytest


Provavelmente, você se deparou com uma estrutura de teste pytest, muitos aqui quase certamente a usam. Eu não sei se você já reparou, mas sob o capô ele também faz alguma mágica.



Por exemplo, você tem um teste tão simples. Se você executá-lo sem pytest, ele lançará um AssertionError simplesmente.



Infelizmente, meu exemplo é um pouco degenerado, e aqui é imediatamente óbvio que len é retirado de uma lista de três elementos. Mas se alguma função fosse chamada, você nunca saberia de um AssertionError que a função retornou. Ela retornou apenas algo que não é igual a cem.



No entanto, se isso for executado no pytest, ele exibirá informações adicionais de depuração. Como ele faz isso por dentro?



Essa mágica funciona de maneira muito simples. O Pytest cria seu próprio gancho especial que é acionado quando o módulo com o teste é carregado. Depois disso, o pytest analisa independentemente esse arquivo Python e, como resultado da análise, é obtida sua representação intermediária, denominada árvore AST. A árvore AST é um conceito básico que permite alterar o código Python rapidamente.

Depois de receber essa árvore, o pytest impõe uma transformação nela, que procura todas as expressões chamadas assert. Ele as altera de uma certa maneira, compila a nova árvore AST resultante e obtém um módulo com testes, que são executados em uma máquina virtual Python comum.



É assim que a árvore AST original não convertida em pytest se parece. A área vermelha destacada é a nossa afirmação. Se você olhar atentamente, verá as partes esquerda e direita, a própria lista.

Quando o pytest converte isso e gera um novo ano, a árvore começa a ficar assim.



Existem cerca de cem linhas de código que o pytest gerou para você.



Se você converter essa árvore AST novamente em Python, será algo parecido com isto. As áreas destacadas em vermelho aqui são onde pytest calcula as partes esquerda e direita da expressão, gera uma mensagem de erro e gera um AssertionError se algo der errado com essa mensagem de erro.

Correspondência de padrões


O que mais você pode fazer com uma coisa dessas? Você pode converter qualquer código Python. E há uma biblioteca maravilhosa que eu encontrei por acidente no PyPI, é interessante cavar lá. Ela faz a correspondência de padrões.



Talvez esse código seja familiar para alguém. Ele considera fatorial recursivamente. Vamos ver como ele pode ser gravado usando a correspondência de padrões.



Para fazer isso, basta pendurar o decorador na função. Atenção: dentro do corpo, a função já funciona de maneira diferente. Cada um desses ifs é uma regra para correspondência de padrões, que analisa a expressão que é inserida na função e a transforma de alguma forma. Além disso, não há retornos explícitos do resultado. Como a biblioteca de padrões, quando transforma o corpo da função, verifica primeiro se ela contém apenas se, e segundo, adiciona retornos implícitos do resultado, alterando assim a semântica da linguagem. Ou seja, ela cria uma nova DSL, que funciona um pouco diferente. E, graças a isso, você pode escrever algumas coisas declarativamente.


A função anterior é como se estivesse escrita em três linhas.





E o restante das linhas adiciona funcionalidade adicional que permite, por exemplo, ler fatorial de uma lista de valores ou passá-lo por uma função arbitrária.

Como escrever conversões você mesmo? macropy!


Agora você provavelmente está se perguntando, mas como pode aplicá-lo você mesmo? Como é tedioso, como pytest: analisar manualmente os arquivos, procure o código que precisa ser convertido. No pytest, isso é feito por um módulo separado para mil ou mais linhas.

Para não fazer isso por conta própria, alguns caras inteligentes já criaram um módulo para nós chamado macropy.

Esta versão do módulo é para o segundo Python e o terceiro. Eles escreveram de volta no tempo do segundo Python. Então os caras fizeram uma piada para descobrir o que pode ser feito com o Python, e a biblioteca inclui vários exemplos. Vamos olhar para eles, eles vão te dar uma idéia do que você pode fazer com esta técnica. A primeira coisa interessante que eles descreveram no tutorial é uma macro que implementa seqüências de formato para o segundo Python, como no terceiro.



A expressão destacada em vermelho é apenas a sintaxe da chamada de macro. A letra S é o nome da macro e, entre colchetes, é a expressão que ela converte. Como resultado, as variáveis ​​são substituídas aqui. Isso funciona no segundo Python, mas o terceiro não é mais necessário em uma macro. Assim, por exemplo, você pode criar sua própria macro, que implementa semânticas mais complexas e faz coisas mais divertidas do que as seqüências de formato padrão.



Quando uma macro se expande, e isso acontece no momento do carregamento do módulo, ela simplesmente se converte nesse código. Os espaços reservados são inseridos na cadeia de formatação e o procedimento de substituição é aplicado a ela. Além disso, o Python, de maneira padrão, já compila tudo isso. No tempo de execução, nenhuma expansão de macro ocorre. Todos eles ocorrem quando o módulo é carregado. Portanto, é possível fazer otimizações ou cálculos que ocorrerão no momento do carregamento do módulo e gerar um bytecode mais ideal.



O segundo exemplo também é interessante. Esta é uma notação abreviada para escrever lambdas. A macro f pega uma série de argumentos e retorna uma função. Cada expressão que começa com o nome da macro “f”, colchetes e, em seguida, absolutamente qualquer expressão é convertida em um lambda.



Na minha opinião, isso também é legal, especialmente para quem gosta de desenvolver e escrever código em um estilo funcional e usar o MapReduce.


Aqui está outro exemplo familiar. Esta função considera fatorial, o código é destacado em vermelho. O que acontecerá quando ela for chamada?



Irá gerar um erro no Python, porque será executado no limite da pilha e haverá um RecursionError tão feio.



Como isso pode ser consertado? Usando macropy, corrigir o problema é muito simples.



Você pendura o decorador, pega o corpo da função e a transforma de alguma maneira mágica. Você não precisa alterar nada na função em si, a macropy fará tudo por você.



E a função retornará a si mesma um resultado bastante normal, indo muito para o subterrâneo.


Como é a macropia?



Ele substitui todas as chamadas para a própria função por um objeto TailCall especial, que é chamado em um loop pelo decorador TCO.



O circuito se parece com isso. O decorador no loop chama a função até retornar algum resultado normal em vez de TailCall. E se ela voltou, então devolve. E isso é tudo. Essas coisas legais podem ser feitas com macros!

Macropy também inclui outros exemplos. Espero que aqueles que têm curiosidade de você os vejam sozinhos. Digamos que há coisas úteis para depuração.



Vou falar sobre outra coisa legal. Um exemplo é essa macro de consulta. O que ele está fazendo? Dentro dele, você escreve código Python regular, que pode ser usado como resultado regular da execução dessa expressão. Mas por dentro, a macropy transforma esse código e o transforma no código da linguagem de consulta SQL Alchemy.



Ele reescreve para você, faz essa expressão terrível. Pode ser reescrito à mão, depois será mais curto. Eu fiz isso.



Aqui está a expressão original. Depois de expandir a macro, assume algo parecido com isto.



Talvez alguém esteja interessado em escrever código mais parecido com o Python, e não forçar seus desenvolvedores a escrever consultas no DSL SQL Alchemy.

Da mesma maneira, você pode gerar qualquer coisa a partir do Python - SQL puro, JavaScript - e salvá-lo em algum lugar próximo ao arquivo e, em seguida, usá-lo no frontend.



Agora vamos ver como fazer sua própria macro. Com macropy, é muito simples.

Uma macro é uma função que pega uma árvore AST na entrada e, de alguma forma, a transforma, retorna uma nova. Aqui está um exemplo de macro que adiciona uma descrição à chamada de declaração que contém a expressão de origem, para que possamos entender por que ocorreu o erro AssertionError.

Aqui, a função replace_assert interna é auxiliar. Ela faz uma descida recursiva em uma árvore para você. Dentro do replace_assert, o elemento da subárvore é passado.



Devido a isso, você pode verificar seu tipo e? se for uma chamada de afirmação, faça algo com ela. Aqui darei um exemplo sintético simples que pega a parte esquerda, a parte direita, envia uma mensagem de erro e grava tudo no atributo msg. Esta é a mensagem que precisará ser retornada.







Ao usá-lo, você anexa essa macro a um bloco de código usando o gerenciador de contexto with, e todo o código que entra no gerenciador de contexto passa por essa transformação. É visto abaixo que nossa mensagem de erro foi adicionada ao AssertionError, que formamos a partir da expressão len ([1, 2, 3]).



No entanto, este método tem uma limitação que me deixa pessoalmente triste. Tentei, como experimento, criar novos designs que funcionem na linguagem. Por exemplo, algumas pessoas gostam de switch ou construções condicionais como a menos que. Infelizmente, isso não é possível: a macropia e outras ferramentas que funcionam com a árvore AST são usadas quando o código-fonte já está lido e dividido em tokens. O código é lido pelo analisador Python, cuja gramática é fixada no intérprete. Para alterá-lo, você precisa recompilar o Python. Obviamente, você pode fazer isso, mas já será um fork do Python, e não uma biblioteca que pode ser definida no PyPI. Portanto, é impossível criar esses desenhos usando macropy.

HyLang


Felizmente, durante toda a minha vida, escrevi não apenas em Python e estava interessado em várias outras linguagens alternativas. Há uma sintaxe que muitos não gostam, mas mais simples e flexível. Essas são expressões s.

Felizmente para nós, existe um suplemento Python chamado HyLang. Isso lembra um pouco o Clojure, apenas o Clojure é executado em cima da JVM e o HyLang é executado em cima da Máquina Virtual Python. Ou seja, ele fornece uma nova sintaxe para escrever código. Mas, ao mesmo tempo, todo o código que você escrever será totalmente compatível com as bibliotecas Python existentes e poderá ser usado nas bibliotecas Python.



Parece algo assim.



A parte à esquerda escrita em Python, à direita - no HyLang. E de baixo para os dois existe um bytecode, que é o resultado. Você provavelmente notou que é exatamente o mesmo, apenas a sintaxe muda. Expressões HyLang, das quais muitos não gostam. Os opositores dos "colchetes" não entendem que essa sintaxe confere ao idioma um tremendo poder, pois uniformiza as construções do idioma. E a uniformidade permite usar macros para implementar qualquer design.

Isso é alcançado devido ao fato de que dentro de cada expressão o primeiro elemento é sempre algum tipo de ação. E então seus argumentos vão.

E todo o código é composto de expressões aninhadas que são fáceis de converter e abrir macros lá. Devido a isso, absolutamente nenhuma construção pode ser feita no HyLang, nova, de forma alguma indistinguível no código dos recursos padrão da linguagem.



Vamos ver como uma macro simples funciona no HyLang. Para fazer o mesmo que fizemos com o Assert usando macropy, você só precisa deste código.

Nossa macro HyLang recebe entrada, que é código. Além disso, uma macro pode facilmente usar qualquer parte desse código para criar um novo código. A principal diferença entre macros e funções: expressões são de entrada, não de valores. Se chamarmos nossa macro como (é (= 1 2)), ela receberá uma expressão (= 1 2) em vez de Falso.



Para que possamos gerar uma mensagem de erro informando que algo deu errado.



E então basta retornar o novo código. Essa sintaxe backtick e til significa algo como o seguinte. A citação anterior diz: pegue esta expressão como está e retorne como está. E o til diz: substitua o valor da variável aqui.



Portanto, quando escrevemos isso, a macro após a expansão retornará para nós uma nova expressão, que será assim afirmada com uma mensagem de erro adicional.

HyLang é uma coisa legal. É verdade que enquanto não o usamos. Talvez nós nunca iremos. Todos esses itens são experimentais. Quero que você saia daqui com a sensação de que, em Python, você pode fazer algumas coisas que talvez nem tenha pensado antes. E talvez alguns deles encontrem aplicação prática em seu trabalho contínuo.

Isso é tudo para mim. Você pode ver os links:

  • Padrões ,
  • MacroPy ,
  • HyLang ,
  • O livro OnLisp - para um estudo avançado dos recursos das macros. Isto é para aqueles especialmente interessados. É verdade que o livro não é inteiramente baseado em Python, mas em Common Lisp. Mas, para um estudo mais aprofundado, isso será até interessante.

All Articles