Makros für einen Pythonisten. Yandex-Bericht

Wie kann ich die Python-Syntax erweitern und die erforderlichen Funktionen hinzufügen? Letzten Sommer habe ich bei PyCon versucht, dieses Thema zu verstehen. Aus dem Bericht können Sie herausfinden, wie die Pytest-, Makropie- und Musterbibliotheken angeordnet sind und wie sie so interessante Ergebnisse erzielen. Am Ende finden Sie ein Beispiel für die Codegenerierung mithilfe von Makros in HyLang, einer Lisp-ähnlichen Sprache, die auf Python ausgeführt wird.


- Hallo Leute. Zunächst möchte ich mich bei den Organisatoren von PyCon bedanken. Ich bin Entwickler bei Yandex. In dem Bericht geht es überhaupt nicht um Arbeit, sondern um experimentelle Dinge. Vielleicht führen sie einen von Ihnen auf die Idee, dass Sie in Python coole Dinge tun können, von denen Sie vorher noch nicht einmal wussten und die nicht in diese Richtung gedacht haben.

Ein wenig für diejenigen, die nicht wissen, was Makros sind: Dies ist eine solche Methode zur Codegenerierung, wenn ein Ausdruck in der Sprache zu komplexerem Code erweitert wird. Was sind die Leckereien für Sie? Für Sie ist der Makrodatensatz prägnant, er drückt eine gewisse Abstraktion aus, aber er erledigt viel Arbeit unter der Haube für Sie, und Sie müssen den gesamten Code nicht mit Ihren Händen schreiben.

pytest


Höchstwahrscheinlich sind Sie auf ein Pytest-Test-Framework gestoßen, das von vielen hier mit ziemlicher Sicherheit verwendet wird. Ich weiß nicht, ob du es jemals bemerkt hast, aber unter der Haube zaubert er auch.



Zum Beispiel haben Sie einen so einfachen Test. Wenn Sie es ohne Pytest ausführen, wird einfach ein AssertionError ausgelöst.



Leider ist mein Beispiel etwas entartet, und hier ist sofort ersichtlich, dass len aus einer Liste von drei Elementen stammt. Wenn jedoch eine Funktion aufgerufen worden wäre, hätten Sie von einem solchen AssertionError niemals gewusst, dass die Funktion zurückgegeben wurde. Sie gab nur etwas zurück, das nicht gleich hundert ist.



Wenn dies jedoch unter pytest ausgeführt wird, werden zusätzliche Debugging-Informationen angezeigt. Wie macht er das drinnen?



Diese Magie funktioniert sehr einfach. Pytest erstellt einen eigenen speziellen Haken, der ausgelöst wird, wenn das Modul mit dem Test geladen wird. Danach analysiert pytest diese Python-Datei unabhängig und als Ergebnis des Parsens wird ihre Zwischendarstellung erhalten, die als AST-Baum bezeichnet wird. Der AST-Baum ist ein Grundkonzept, mit dem Sie Python-Code im laufenden Betrieb ändern können.

Nach dem Empfang eines solchen Baums legt pytest eine Transformation fest, die nach allen Ausdrücken sucht, die als assert bezeichnet werden. Er ändert sie auf eine bestimmte Weise, kompiliert den resultierenden neuen AST-Baum und erhält ein Modul mit Tests, das dann auf einer regulären Python Virtual Machine ausgeführt wird.



So sieht der ursprüngliche AST-Baum aus, der nicht in Pytest konvertiert wurde. Der hervorgehobene rote Bereich ist unser Assert. Wenn Sie genau hinschauen, sehen Sie den linken und rechten Teil, die Liste selbst.

Wenn pytest dies umwandelt und ein neues Jahr generiert, sieht der Baum so aus.



Es gibt ungefähr hundert Codezeilen, die pytest für Sie generiert hat.



Wenn Sie diesen AST-Baum wieder in Python konvertieren, sieht er ungefähr so ​​aus. In den hier rot hervorgehobenen Bereichen berechnet pytest den linken und rechten Teil des Ausdrucks, generiert eine Fehlermeldung und löst einen AssertionError aus, wenn bei dieser Fehlermeldung ein Fehler aufgetreten ist.

Mustervergleich


Was kann man mit so etwas noch machen? Sie können jeden Python-Code konvertieren. Und es gibt eine wundervolle Bibliothek, die ich ganz zufällig auf PyPI gefunden habe. Es ist interessant, dort zu graben. Sie macht Mustervergleich.



Vielleicht ist dieser Code jemandem bekannt. Er betrachtet faktoriell rekursiv. Mal sehen, wie es mit Pattern Matching aufgezeichnet werden kann.



Hängen Sie dazu einfach den Dekorateur an die Funktion. Bitte beachten Sie: Im Körper funktioniert die Funktion bereits anders. Jedes dieser ifs ist eine Regel für den Mustervergleich, die den in die Funktion eingegebenen Ausdruck analysiert und ihn irgendwie transformiert. Darüber hinaus gibt es nicht einmal explizite Rückgaben des Ergebnisses. Da die Musterbibliothek beim Transformieren des Funktionskörpers erstens prüft, ob sie nur dann enthält, wenn sie zweitens implizite Rückgaben des Ergebnisses hinzufügt, wodurch die Semantik der Sprache geändert wird. Das heißt, sie macht ein neues DSL, das etwas anders funktioniert. Und dank dessen können Sie einige Dinge deklarativ aufschreiben.


Die vorherige Funktion ist wie in drei Zeilen geschrieben.





Der Rest der Zeilen fügt zusätzliche Funktionen hinzu, mit denen beispielsweise Fakultäten aus einer Werteliste gelesen oder durch eine beliebige Funktion geleitet werden können.

Wie schreibe ich Conversions selbst? Makropie!


Jetzt wundern Sie sich wahrscheinlich, aber wie können Sie es selbst anwenden? Suchen Sie nach dem Code, der konvertiert werden muss, da es mühsam ist, wie z. B. pytest: Dateien manuell zu analysieren. Im Pytest wird dies durch ein separates Modul für tausend oder mehr Zeilen durchgeführt.

Um dies nicht alleine zu tun, haben einige clevere Leute bereits ein Modul für uns entwickelt, das sich Makropie nennt.

Diese Version des Moduls ist sowohl für den zweiten Python als auch für den dritten. Sie haben es in der Zeit des zweiten Python geschrieben. Dann hatten die Jungs einen Witz, um herauszufinden, was mit Python gemacht werden kann, und die Bibliothek enthält verschiedene Beispiele. Schauen wir sie uns an, sie geben Ihnen eine Vorstellung davon, was Sie mit dieser Technik tun können. Die erste coole Sache, die sie im Tutorial beschrieben haben, ist ein Makro, das Formatzeichenfolgen für das zweite Python implementiert, wie im dritten.



Der rot hervorgehobene Ausdruck ist nur die Syntax des Makroaufrufs. Der Buchstabe S ist der Name des Makros und in eckigen Klammern der Ausdruck, den es konvertiert. Infolgedessen werden hier Variablen ersetzt. Dies funktioniert im zweiten Python, aber der dritte wird in einem solchen Makro nicht mehr benötigt. So können Sie beispielsweise Ihr eigenes Makro erstellen, das eine komplexere Semantik implementiert und mehr Spaß macht als Zeichenfolgen im Standardformat.



Wenn ein Makro erweitert wird und dies zum Zeitpunkt des Ladens des Moduls geschieht, wird es einfach in diesen Code konvertiert. Platzhalter werden in die Formatzeichenfolge eingefügt und das Ersetzungsverfahren wird darauf angewendet. Weiteres Python kompiliert dies alles bereits in Standardform. Zur Laufzeit treten keine Makroerweiterungen auf. Alle treten auf, wenn das Modul geladen wird. Daher können Sie auf diese Weise sogar Optimierungen oder Berechnungen vornehmen, die zum Zeitpunkt des Ladens des Moduls erfolgen, und einen optimaleren Bytecode generieren.



Das zweite Beispiel ist ebenfalls interessant. Dies ist eine Kurzschreibweise zum Schreiben von Lambdas. Das Makro f verwendet eine Reihe von Argumenten und gibt stattdessen eine Funktion zurück. Jeder Ausdruck, der mit dem Makronamen „f“, Klammern und dann absolut jedem Ausdruck beginnt, wird in ein Lambda konvertiert.



Meiner Meinung nach ist dies auch cool, insbesondere für diejenigen, die Code in einem funktionalen Stil entwickeln und schreiben und MapReduce verwenden möchten.


Hier ist ein weiteres bekanntes Beispiel. Diese Funktion wird als Fakultät betrachtet, der Code wird rot hervorgehoben. Was passiert, wenn sie angerufen wird?



In Python wird ein Fehler ausgegeben, da das Stack-Limit überschritten wird und ein so hässlicher RecursionError auftritt.



Wie kann das behoben werden? Mit Macropy ist die Behebung des Problems sehr einfach.



Sie hängen den Dekorateur auf, er nimmt den Körper der Funktion und transformiert ihn auf magische Weise. Sie müssen nichts an der Funktion selbst ändern, Macropy erledigt alles für Sie.



Und die Funktion wird zu einem ganz normalen Ergebnis zurückkehren und weit in den Untergrund gehen.


Wie makropy macht das?



Es ersetzt alle Aufrufe der Funktion selbst durch ein spezielles TailCall-Objekt, das dann vom TCO-Dekorator in einer Schleife aufgerufen wird.



Die Schaltung sieht ungefähr so ​​aus. Der Dekorator in der Schleife ruft die Funktion auf, bis anstelle von TailCall ein normales Ergebnis zurückgegeben wird. Und wenn sie zurückkommt, dann gibt sie es zurück. Und alle. Diese coolen Dinge können mit Makros gemacht werden!

Makropie enthält auch andere Beispiele. Ich hoffe, diejenigen, die neugierig auf dich sind, gehen und sehen sie alleine. Nehmen wir an, es gibt nützliche Dinge zum Debuggen.



Ich erzähle dir von einer anderen coolen Sache. Ein Beispiel ist dieses Abfragemakro. Was macht er? Darin schreiben Sie regulären Python-Code, den Sie dann als reguläres Ergebnis der Ausführung dieses Ausdrucks verwenden können. Im Inneren transformiert Macropy diesen Code und macht ihn zu Alchemy SQL-Abfragesprachencode.



Er schreibt es für dich um, macht diesen schrecklichen Ausdruck. Es kann von Hand umgeschrieben werden, dann wird es kürzer. Ich habe es gemacht.



Hier ist der ursprüngliche Ausdruck. Nach dem Erweitern des Makros nimmt es ungefähr so ​​an.



Vielleicht ist jemand daran interessiert, Code zu schreiben, der Python ähnlicher ist, und seine Entwickler nicht zu zwingen, Abfragen in DSL SQL Alchemy zu schreiben.

Auf die gleiche Weise können Sie alles aus Python generieren - reines SQL, JavaScript - und es irgendwo neben der Datei speichern und dann im Frontend verwenden.



Nun wollen wir sehen, wie Sie Ihr eigenes Makro erstellen. Mit Makropie ist es sehr einfach.

Ein Makro ist eine Funktion, die einen AST-Baum an der Eingabe verwendet und auf irgendeine Weise einen neuen zurückgibt. Hier ist ein Makrobeispiel, das dem Assert-Aufruf, der den Quellausdruck enthält, eine Beschreibung hinzufügt, damit wir verstehen, warum der AssertionError-Fehler aufgetreten ist.

Hier ist die interne Funktion replace_assert hilfreich. Sie macht für Sie einen rekursiven Abstieg in einen Baum. Innerhalb des replace_assert wird das Teilbaumelement übergeben.



Aus diesem Grund können Sie den Typ von innen überprüfen und? Wenn es sich um einen Assert-Aufruf handelt, machen Sie etwas damit. Hier werde ich ein einfaches synthetisches Beispiel geben, das den linken Teil, den rechten Teil nimmt, eine Fehlermeldung von ihnen macht und alles in das msg-Attribut schreibt. Dies ist die Nachricht, die zurückgegeben werden muss.







Wenn Sie es verwenden, hängen Sie ein solches Makro mithilfe des with context-Managers an einen Codeblock an, und der gesamte Code, der in den Kontextmanager gelangt, durchläuft diese Umwandlung. Es ist unten zu sehen, dass unsere Fehlermeldung zum AssertionError hinzugefügt wurde, den wir aus dem len-Ausdruck ([1, 2, 3]) gebildet haben.



Diese Methode hat jedoch eine Einschränkung, die mich persönlich traurig macht. Ich habe als Experiment versucht, neue Designs zu erstellen, die in der Sprache funktionieren. Zum Beispiel mögen manche Leute Schalter oder bedingte Konstruktionen wie es sei denn. Dies ist jedoch leider nicht möglich: Makropie und andere Tools, die mit dem AST-Baum arbeiten, werden verwendet, wenn der Quellcode bereits gelesen und in Token aufgeteilt wurde. Der Code wird vom Python-Parser gelesen, dessen Grammatik im Interpreter festgelegt ist. Um dies zu ändern, müssen Sie Python neu kompilieren. Natürlich können Sie dies tun, aber es wird bereits eine Abzweigung von Python sein und keine Bibliothek, die auf PyPI angelegt werden kann. Daher ist es unmöglich, solche Konstruktionen unter Verwendung von Makropie herzustellen.

HyLang


Glücklicherweise schrieb ich für mein langes Leben nicht nur in Python und interessierte mich für verschiedene andere alternative Sprachen. Es gibt eine Syntax, die viele nicht mögen, die aber einfacher und flexibler ist. Dies sind S-Ausdrücke.

Zum Glück gibt es ein Python-Add-In namens HyLang. Dieses Ding erinnert etwas an Clojure, nur Clojure läuft auf der JVM und HyLang läuft auf der Python Virtual Machine. Das heißt, es bietet Ihnen eine neue Syntax zum Schreiben von Code. Gleichzeitig ist der gesamte von Ihnen geschriebene Code vollständig mit vorhandenen Python-Bibliotheken kompatibel und kann aus Python-Bibliotheken verwendet werden.



Es sieht ungefähr so ​​aus.



Der Teil links in Python, rechts in HyLang. Und von unten ist für beide ein Bytecode, der das Ergebnis ist. Sie haben wahrscheinlich bemerkt, dass es genau das gleiche ist, nur die Syntax ändert sich. HyLang S-Ausdrücke, die viele nicht mögen. Gegner der „Klammern“ verstehen nicht, dass eine solche Syntax der Sprache eine enorme Kraft verleiht, weil sie den Konstruktionen der Sprache Einheitlichkeit verleiht. Durch die Einheitlichkeit können Sie Makros verwenden, um jedes Design zu implementieren.

Dies wird dadurch erreicht, dass in jedem Ausdruck das erste Element immer eine Art Aktion ist. Und dann gehen seine Argumente.

Der gesamte Code besteht aus verschachtelten Ausdrücken, die einfach konvertiert und dort geöffnet werden können. Aus diesem Grund können in HyLang absolut alle Konstruktionen erstellt werden, die neu sind und im Code in keiner Weise von den Standardfunktionen der Sprache zu unterscheiden sind.



Mal sehen, wie ein einfaches Makro in HyLang funktioniert. Um dasselbe zu tun, was wir mit Assert unter Verwendung von Makropy gemacht haben, benötigen Sie nur diesen Code.

Unser HyLang-Makro empfängt Eingaben, bei denen es sich um Code handelt. Darüber hinaus kann ein Makro problemlos einen beliebigen Teil dieses Codes verwenden, um neuen Code zu erstellen. Der Hauptunterschied zwischen Makros und Funktionen: Ausdrücke sind Eingaben, keine Werte. Wenn wir unser Makro als (is (= 1 2)) aufrufen, erhält es einen Ausdruck (= 1 2) anstelle von False.



So können wir eine Fehlermeldung generieren, dass etwas schief gelaufen ist.



Und dann einfach den neuen Code zurückgeben. Diese Backtick- und Tilde-Syntax bedeutet ungefähr Folgendes. Das hintere Zitat sagt: Nimm diesen Ausdruck wie er ist und gib ihn zurück wie er ist. Und die Tilde sagt: Ersetzen Sie hier den Wert der Variablen.



Wenn wir dies schreiben, gibt das Makro bei der Erweiterung einen neuen Ausdruck an uns zurück, der dadurch mit einer zusätzlichen Fehlermeldung bestätigt wird.

HyLang ist eine coole Sache. Stimmt, während wir es nicht benutzen. Vielleicht werden wir es nie tun. Alle diese Punkte sind experimentell. Ich möchte, dass Sie hier mit dem Gefühl abreisen, dass Sie in Python einige Dinge tun können, an die Sie vorher vielleicht noch nicht einmal gedacht haben. Und vielleicht finden einige von ihnen praktische Anwendung in Ihrer laufenden Arbeit.

Das ist alles für mich. Sie können die Links sehen:

  • Muster ,
  • MacroPy ,
  • HyLang ,
  • Das Buch OnLisp - für eine fortgeschrittene Untersuchung der Fähigkeiten von Makros. Dies ist für diejenigen, die besonders interessiert sind. Das Buch basiert zwar nicht ausschließlich auf Python, sondern auf Common Lisp. Für eine eingehendere Untersuchung wird dies jedoch sogar interessant sein.

All Articles