Macros pour un pythoniste. Rapport Yandex

Comment puis-je étendre la syntaxe Python et y ajouter les fonctionnalités nécessaires? L'été dernier à PyCon, j'ai essayé de comprendre ce sujet. À partir du rapport, vous pouvez découvrir comment les bibliothèques de pytest, de macropie et de modèles sont organisées et comment elles permettent d'obtenir des résultats aussi intéressants. À la fin, il y a un exemple de génération de code à l'aide de macros dans HyLang, un langage de type Lisp fonctionnant au-dessus de Python.


- Salut les gars. Tout d'abord, je tiens à remercier les organisateurs de PyCon. Je suis développeur chez Yandex. Le rapport ne portera pas du tout sur le travail, mais sur des choses expérimentales. Peut-être qu'ils conduiront l'un d'entre vous à l'idée qu'en Python, vous pouvez faire des choses cool que vous ne connaissiez même pas auparavant, ne pensiez pas dans cette direction.

Un peu pour ceux qui ne savent pas ce que sont les macros: c'est un tel moyen de génération de code quand une expression du langage est développée en code plus complexe. Quels sont les goodies pour vous? Pour vous, l'enregistrement de macro est concis, il exprime une certaine abstraction, mais il fait beaucoup de travail sous le capot pour vous, et vous n'avez pas besoin d'écrire tout ce code avec vos mains.

pytest


Très probablement, vous êtes tombé sur un cadre de test pytest, beaucoup ici l'utilisent presque certainement. Je ne sais pas si vous l'avez déjà remarqué, mais sous le capot, il fait aussi de la magie.



Par exemple, vous avez un test aussi simple. Si vous l'exécutez sans pytest, il lancera simplement une AssertionError.



Malheureusement, mon exemple est un peu dégénéré, et ici, il est immédiatement évident que len est tiré d'une liste de trois éléments. Mais si une fonction était appelée, vous n'auriez jamais su d'une telle AssertionError que la fonction était retournée. Elle a rendu juste quelque chose qui n'est pas égal à cent.



Cependant, si cela est exécuté sous pytest, il affichera des informations de débogage supplémentaires. Comment fait-il à l'intérieur?



Cette magie fonctionne très simplement. Pytest crée son propre crochet spécial qui se déclenche lorsque le module avec le test se charge. Après cela, pytest analyse indépendamment ce fichier Python, et à la suite de l'analyse, sa représentation intermédiaire est obtenue, qui est appelée l'arbre AST. L'arbre AST est un concept de base qui vous permet de modifier le code Python à la volée.

Après avoir reçu un tel arbre, pytest lui impose une transformation qui recherche toutes les expressions appelées assert. Il les modifie d'une certaine manière, il compile la nouvelle arborescence AST résultante, et il obtient un module avec des tests, qui s'exécute ensuite sur une machine virtuelle Python régulière.



Voici à quoi ressemble l'arbre AST d'origine non converti en pytest. La zone rouge en surbrillance est notre assertion. Si vous regardez attentivement, vous verrez ses parties gauche et droite, la liste elle-même.

Lorsque pytest convertit cela et génère une nouvelle année, l'arbre commence à ressembler à ceci.



Il existe une centaine de lignes de code générées par pytest pour vous.



Si vous reconvertissez cet arbre AST en Python, il ressemblera à ceci. Les zones surlignées en rouge ici sont celles où pytest calcule les parties gauche et droite de l'expression, génère un message d'erreur et déclenche une AssertionError en cas de problème avec ce message d'erreur.

Correspondance de motifs


Que pouvez-vous faire d'autre avec une telle chose? Vous pouvez convertir n'importe quel code Python. Et il y a une merveilleuse bibliothèque que j'ai trouvée tout à fait par accident sur PyPI, c'est intéressant d'y creuser. Elle fait la correspondance des motifs.



Peut-être que ce code est familier à quelqu'un. Il considère factorielle récursivement. Voyons comment il peut être enregistré en utilisant la correspondance de motifs.



Pour ce faire, accrochez simplement le décorateur sur la fonction. Attention: à l'intérieur du corps, la fonction fonctionne déjà différemment. Chacun de ces ifs est une règle pour la correspondance de modèle, qui analyse l'expression entrée dans la fonction et la transforme en quelque sorte. De plus, il n'y a même pas de retour explicite du résultat. Parce que la bibliothèque de modèles, lorsqu'elle transforme le corps de la fonction, d'une part, elle vérifie qu'elle ne contient que si, et d'autre part, elle ajoute des retours implicites du résultat, changeant ainsi la sémantique du langage. Autrement dit, elle fait une nouvelle DSL, qui fonctionne un peu différemment. Et grâce à cela, vous pouvez écrire certaines choses de manière déclarative.


La fonction précédente est comme si elle était écrite sur trois lignes.





Et le reste des lignes ajoute des fonctionnalités supplémentaires qui permettent, par exemple, de lire factorielle à partir d'une liste de valeurs ou de la passer par une fonction arbitraire.

Comment écrire des conversions vous-même? macropie!


Maintenant vous vous demandez probablement, mais comment pouvez-vous l'appliquer vous-même? Parce que c'est fastidieux à faire, comme pytest: analyser manuellement des fichiers, recherchez le code qui doit être converti. Dans pytest, cela se fait par un module séparé pour un millier de lignes ou plus.

Afin de ne pas le faire par nous-mêmes, certains gars intelligents ont déjà créé un module pour nous appelé macropy.

Cette version du module est destinée à la fois au deuxième Python et au troisième. Ils l'ont écrit à l'époque du deuxième Python. Ensuite, les gars ont fait une blague pour comprendre ce qui peut être fait avec Python, et la bibliothèque comprend divers exemples. Regardons-les, ils vous donneront une idée de ce que vous pouvez faire avec cette technique. La première chose intéressante qu'ils ont décrite dans le didacticiel est une macro qui implémente des chaînes de format pour le deuxième Python, comme dans le troisième.



L'expression surlignée en rouge n'est que la syntaxe de l'appel de macro. La lettre S est le nom de la macro, puis entre crochets l'expression qu'elle convertit. Par conséquent, les variables sont remplacées ici. Cela fonctionne dans le deuxième Python, mais le troisième n'est plus nécessaire dans une telle macro. Ainsi, par exemple, vous pouvez créer votre propre macro, qui implémente une sémantique plus complexe et fait des choses plus amusantes que les chaînes de format standard.



Lorsqu'une macro se développe, et cela se produit au moment du chargement du module, elle se convertit simplement en ce code. Des espaces réservés sont insérés dans la chaîne de format et la procédure de substitution lui est appliquée. De plus, Python compile déjà de manière standard tout cela. Au moment de l'exécution, aucune extension de macro ne se produit. Tous se produisent lorsque le module est chargé. Par conséquent, sur une telle chose, vous pouvez même effectuer des optimisations ou des calculs qui se produiront au moment du chargement du module et générer un bytecode plus optimal.



Le deuxième exemple est également intéressant. Il s'agit d'une notation abrégée pour écrire des lambdas. La macro f prend une série d'arguments et renvoie une fonction à la place. Chaque expression commençant par le nom de macro «f», les crochets, puis absolument toute expression est convertie en lambda.



À mon avis, c'est aussi cool, surtout pour ceux qui aiment développer et écrire du code dans un style fonctionnel et utiliser MapReduce.


Voici un autre exemple familier. Cette fonction est factorielle, le code est surligné en rouge. Que se passera-t-il lorsqu'elle sera appelée?



Il générera une erreur en Python, car il s'exécutera dans la limite de pile et il y aura une telle RecursionError laide.



Comment résoudre ce problème? En utilisant la macropie, la résolution du problème est très simple.



Vous accrochez le décorateur, il prend le corps de la fonction et le transforme de façon magique. Vous n'avez rien à changer dans la fonction elle-même, la macropie fera tout pour vous.



Et la fonction reviendra à elle-même un résultat tout à fait normal, allant loin dans le métro.


Comment fonctionne la macropie?



Il remplace tous les appels à la fonction elle-même par un objet TailCall spécial, qui est ensuite appelé en boucle par le décorateur TCO.



Le circuit ressemble à ceci. Le décorateur de la boucle appelle la fonction jusqu'à ce qu'elle renvoie un résultat normal au lieu de TailCall. Et si elle est revenue, elle le retourne. Et c'est tout. Ces choses sympas peuvent être faites avec des macros!

Macropy comprend également d'autres exemples. J'espère que ceux qui sont curieux de vous voir par eux-mêmes. Disons qu'il y a des choses utiles pour le débogage.



Je vais vous parler d'une autre chose sympa. Un exemple est cette macro de requête. Que fait-il? À l'intérieur, vous écrivez du code Python standard, que vous pouvez ensuite utiliser comme résultat régulier de l'exécution de cette expression. Mais à l'intérieur, macropy transforme ce code et le transforme en code de langage de requête Alchemy SQL.



Il le réécrit pour vous, fait cette terrible expression. Il peut être réécrit à la main, puis il sera plus court. Je l'ai fait.



Voici l'expression originale. Après avoir développé la macro, elle prend quelque chose comme ça.



Peut-être que quelqu'un est intéressé à écrire du code plus similaire à Python et à ne pas forcer ses développeurs à écrire des requêtes sur DSL SQL Alchemy.

De la même manière, vous pouvez générer n'importe quoi à partir de Python - SQL pur, JavaScript - et l'enregistrer quelque part à côté du fichier, puis l'utiliser sur le frontend.



Voyons maintenant comment créer votre propre macro. Avec la macropie, c'est très simple.

Une macro est une fonction qui prend un arbre AST à l'entrée et, en quelque sorte le transformant, en retourne un nouveau. Voici un exemple de macro qui ajoute une description à l'appel d'assertion contenant l'expression source afin que nous puissions comprendre pourquoi l'erreur AssertionError s'est produite.

Ici, la fonction interne replace_assert est helper. Elle fait une descente récursive dans un arbre pour vous. À l'intérieur de replace_assert, l'élément subtree est passé.



Pour cette raison, vous pouvez vérifier à l'intérieur son type et? s'il s'agit d'un appel Assert, faites-en quelque chose. Ici, je vais donner un exemple synthétique simple qui prend la partie gauche, la partie droite, en fait un message d'erreur et écrit tout dans l'attribut msg. C'est le message qui devra être renvoyé.







Lorsque vous l'utilisez, vous attachez une telle macro à un bloc de code à l'aide du gestionnaire de contexte with, et tout le code qui pénètre dans le gestionnaire de contexte passe par cette transformation. On voit ci-dessous que notre message d'erreur a été ajouté à AssertionError, que nous avons formé à partir de l'expression len ([1, 2, 3]).



Cependant, cette méthode a une limitation qui me rend personnellement triste. J'ai essayé comme expérience de faire de nouveaux designs qui fonctionneront dans la langue. Par exemple, certaines personnes aiment le commutateur ou les constructions conditionnelles comme à moins que. Mais malheureusement, cela n'est pas possible: la macropie et tous les autres outils qui fonctionnent avec l'arbre AST sont utilisés lorsque le code source est déjà lu et divisé en jetons. Le code est lu par l'analyseur Python, dont la grammaire est fixée dans l'interpréteur. Pour le changer, vous devez recompiler Python. Bien sûr, vous pouvez le faire, mais ce sera déjà un fork de Python, et non une bibliothèque qui peut être présentée sur PyPI. Par conséquent, il est impossible de réaliser de telles constructions en utilisant la macropie.

HyLang


Heureusement, pendant ma longue vie, j'ai écrit non seulement en Python et j'étais intéressé par divers autres langages alternatifs. Il existe une syntaxe que beaucoup n'aiment pas, mais plus simple et flexible. Ce sont des expressions s.

Heureusement pour nous, il existe un complément Python appelé HyLang. Cette chose rappelle quelque peu Clojure, seul Clojure s'exécute au-dessus de la JVM et HyLang s'exécute au-dessus de la machine virtuelle Python. Autrement dit, il vous fournit une nouvelle syntaxe pour écrire du code. Mais en même temps, tout le code que vous écrivez sera entièrement compatible avec les bibliothèques Python existantes, et il peut être utilisé à partir des bibliothèques Python.



Cela ressemble à ceci.



La partie de gauche écrite en Python, à droite - sur HyLang. Et du bas pour les deux est un bytecode, qui est le résultat. Vous avez probablement remarqué que c'est exactement la même chose, seule la syntaxe change. Les expressions s HyLang, que beaucoup n'aiment pas. Les opposants aux «crochets» ne comprennent pas qu'une telle syntaxe donne à la langue un pouvoir énorme car elle donne l'uniformité aux constructions de la langue. Et l'uniformité vous permet d'utiliser des macros pour implémenter n'importe quelle conception.

Cela est dû au fait qu'à l'intérieur de chaque expression, le premier élément est toujours une sorte d'action. Et puis ses arguments disparaissent.

Et tout le code est composé d'expressions imbriquées faciles à convertir et à ouvrir des macros. Pour cette raison, absolument toutes les constructions peuvent être faites en HyLang, nouvelles, en aucune façon indiscernables des fonctionnalités de langage standard dans le code.



Voyons comment fonctionne une macro simple sur HyLang. Pour faire la même chose que nous avons fait avec Assert en utilisant la macropie, vous n'avez besoin que de ce code.

Notre macro HyLang reçoit une entrée, qui est du code. En outre, une macro peut facilement utiliser n'importe quelle partie de ce code pour créer un nouveau code. La principale différence entre les macros et les fonctions: les expressions sont entrées, pas des valeurs. Si nous appelons notre macro comme (is (= 1 2)), elle recevra alors une expression (= 1 2) au lieu de False.



Nous pouvons donc générer un message d'erreur indiquant que quelque chose s'est mal passé.



Et puis retournez simplement le nouveau code. Cette syntaxe backtick et tilde signifie quelque chose comme ce qui suit. La citation arrière dit: prenez cette expression telle quelle et retournez-la telle quelle. Et le tilde dit: remplacez la valeur de la variable ici.



Par conséquent, lorsque nous écrivons ceci, la macro lors de l'expansion nous renverra une nouvelle expression, qui sera ainsi affirmée avec un message d'erreur supplémentaire.

HyLang est une chose cool. C'est vrai, alors que nous ne l'utilisons pas. Peut-être que nous ne le ferons jamais. Tous ces éléments sont expérimentaux. Je veux que vous quittiez ici avec le sentiment qu'en Python, vous pouvez faire des choses auxquelles vous n'aviez peut-être même pas pensé auparavant. Et peut-être que certains d'entre eux trouveront une application pratique dans votre travail en cours.

C’est tout pour moi. Vous pouvez voir les liens:

  • Patterns ,
  • MacroPy ,
  • HyLang ,
  • Le livre OnLisp - pour une étude avancée des capacités des macros. C'est pour ceux qui sont particulièrement intéressés. Certes, le livre n'est pas entièrement basé sur Python, mais sur Common Lisp. Mais pour une étude plus approfondie, ce sera encore plus intéressant.

All Articles