Nutzlose REPL. Yandex-Bericht

REPL (read-eval-print loop) ist in Python nutzlos, selbst wenn es sich um magisches IPython handelt. Heute werde ich eine der möglichen Lösungen für dieses Problem anbieten. Zuallererst werden der Bericht und meine Erweiterung TheREPL für diejenigen nützlich sein, die an einer schnelleren und effizienteren Entwicklung interessiert sind, sowie für diejenigen, die Stateful-Systeme schreiben.


- Mein Name ist Alexander, ich arbeite als Programmierer in Yandex. Wir schreiben in meinem Team in Python, wir haben noch nicht auf Go umgestellt. Aber in meiner Freizeit programmiere und mache ich es seltsamerweise auch in einer sehr dynamischen Sprache - Common Lisp. Es ist vielleicht noch dynamischer als Python. Ihre Besonderheit liegt darin, dass der Entwicklungsprozess selbst etwas anders angeordnet ist. Es ist interaktiver und iterativer, da Sie in REPL auf Lisp alles tun können: neue Module erstellen und alte löschen, Methoden, Klassen hinzufügen und löschen, Klassen neu definieren usw.



In Python ist dies umso schwieriger. Es hat IPython. Natürlich verbessert IPython REPL in gewisser Weise, fügt die automatische Vervollständigung hinzu und ermöglicht die Verwendung verschiedener Erweiterungen. Aber für die iterative Entwicklung passt es nicht sehr gut. Darin können Sie den Code herunterladen, ein wenig testen und fertig. Und manchmal möchte er mehr Interaktivität, damit Sie diese REPL wirklich in der Entwicklung verwenden, zwischen Modulen wechseln, Funktionen und Klassen in ihnen ändern können.

Es passiert mir - Sie führen beispielsweise IPython REPL in der Produktionsumgebung aus und beginnen dort, einige Befehle auszuführen, etwas zu untersuchen, und dann stellt sich heraus, dass im Modul ein Fehler vorliegt, und Sie möchten ihn schnell beheben. Dies funktioniert jedoch nicht, da Sie ein neues Docker-Image erstellen, es in die Produktion einbinden, erneut in diese REPL wechseln, dort erneut den gewünschten Status erreichen und alles, was darauf fiel, erneut starten müssen. Und im Idealfall müsste ich die Funktion reparieren, sofort ausführen und sofort das Ergebnis erhalten.

Was kann man dagegen tun? Wie kann ich Code in IPython neu laden? Ich habe versucht, Autoreload zu verwenden, und es hat mir aus mehreren Gründen nicht gefallen. Wenn das Modul neu gestartet wird, verliert es zunächst den Status, der sich in den globalen Variablen in diesem Modul befand. Und es kann einen zwischengespeicherten Wert mit den Ergebnissen einiger Funktionen geben. Oder ich könnte dort beispielsweise Daten über das Netzwerk laden, um später schneller damit arbeiten zu können. Das heißt, das automatische Laden verliert den Status.

Daher habe ich als Experiment meine einfache Erweiterung für IPython erstellt und sie TheREPL genannt.

Ich bin mit diesem Bericht zu Ihnen gekommen, um eine Vorstellung davon zu bekommen, was mit REPL in Python getan werden kann. Und ich hoffe wirklich, dass Ihnen diese Idee gefällt, Sie sie in Ihrem Kopf umsetzen und weiterhin Dinge entwickeln, die Python noch effizienter und bequemer machen.

Was ist TheREPL? Dies ist die Erweiterung, die Sie herunterladen. Danach wird in IPython ein Konzept wie der Namespace angezeigt. Sie können zu jedem Python-Modul wechseln, um zu sehen, welche Variablen, Funktionen usw. vorhanden sind. Und was noch wichtiger ist: Sie können def, den Namen der Funktion, direkt schreiben, die Funktion oder Klasse neu definieren und sie ändert sich in allen Modulen, in die sie importiert wurde. Gleichzeitig wird das Modul selbst nicht neu gestartet, sodass der Status gespeichert wird. Darüber hinaus können Sie mit TheREPL weitere Artefakte vermeiden, die sich beim automatischen Laden befinden und die wir uns jetzt ansehen werden.



Beim automatischen Laden erfolgt die Code-Aktualisierung nur, wenn die Datei gespeichert wird. Gleichzeitig müssen Sie jedoch etwas in die REPL selbst eingeben, und erst dann übernimmt das automatische Laden diese Änderungen. Dies ist Problem Nummer 1. Das heißt, wenn Sie einen Hintergrundprozess in einem separaten Thread haben (z. B. wenn der Server ausgeführt wird), können Sie den Code nicht einfach übernehmen und korrigieren. Beim Autoreload werden diese Änderungen erst angewendet, wenn Sie etwas in die IPython-REPL eingeben.

Bei meiner Erweiterung drücken Sie die Verknüpfung rechts im Editor, und die Funktion unter dem Cursor wird sofort angewendet und beginnt zu arbeiten. Das heißt, mit TheREPL können Sie den Code genauer ändern. Sie können def auch in IPython schreiben.



Das Umschalten zwischen Modulen wird, wie gesagt, in keiner Weise unterstützt. Sie können die Datei nur im Dateisystem finden, ändern und hoffen, dass das automatische Laden alles dort auflöst.



Weiter. Autoreload verliert globale Variablen, TheREPL speichert und ermöglicht es Ihnen, den Betrieb Ihrer Anwendung weiter zu untersuchen, ihren internen Code zu ändern und sie so schnell zu entwickeln.



Autoreload verfügt weiterhin über diese Funktion. Er nimmt sehr geschickt Änderungen an dem Modul vor, das neu geladen wird. Insbesondere macht er dort einen sehr interessanten Trick. Wenn die Funktion in diesem Modul aktualisiert wurde, verwendet er den Garbage Collector, um sie und alle diese Instanzen von Funktionen zu finden und den darin enthaltenen Code zu ändern, um sie überall dort zu ändern, wo sie importiert wurde. Weiter werden wir uns Beispiele ansehen, wie dies geschieht. Aus diesem Grund ändert sich der Funktionscode, auch wenn er in den Verschluss gelangt.

Wissen Sie, was eine Schließung ist? Dies ist eine sehr nützliche Sache. JavaScript-Entwickler verwenden dies ständig. Sie haben höchstwahrscheinlich auch einfach nie aufgepasst. Da das automatische Laden jedoch das tut, was ich oben beschrieben habe, befinden Sie sich möglicherweise in einer Situation, in der der alte Code neuen Code verwendet, der möglicherweise anders funktioniert. Beispielsweise kann eine Funktion nicht einen Wert zurückgeben, sondern zwei, Tupel anstelle von Zeichenfolge usw. Der alte Code wird dabei unterbrochen.

TheREPL macht keine so kniffligen Dinge, um sicherzustellen, dass alles konsistenter ist. Das heißt, es ändert die Funktion oder Klasse in dem Modul, in dem es definiert ist. Findet diese Klasse in allen anderen Modulen und ändert sie auch dort. Danach funktioniert alles auf eine neue Art und Weise.



Wie funktioniert das Ersetzen der Funktion beim automatischen Laden? Wir haben zwei Funktionen, eine und zwei. Jede Funktion verfügt über eine Reihe von Attributen: Dokumentation, Code, Argumente usw. Hier auf der Folie finden Sie ein Beispiel für das Ersetzen der Attribute, in denen der Bytecode gespeichert ist.

Nachdem das automatische Laden es geändert hat, funktioniert die aufgerufene Funktion anders. Aber dies ist ein synthetisches Beispiel, das ich gerade mit meinen Händen reproduziert habe, damit Sie verstehen, was passiert. Die Funktion wird auf eine Weise aufgerufen, aber der Code dort ist tatsächlich anders. Und wenn Sie zerlegen, zeigt es auch, dass es eine Zwei zurückgibt. Was führt das?



Hier ist ein Beispiel für eine Schließung. In der zweiten Zeile erstellen wir einen Abschluss, in dem wir die Funktion foo erfassen. Der Abschluss selbst erwartet, dass diese Funktion, die wir übergeben haben, einen String zurückgibt, ihn in utf-8 codiert und alles funktioniert.



Angenommen, Sie ändern das Modul, in dem foo definiert ist, und das automatische Laden übernimmt die Änderung. Und Sie ändern es so, dass es keine Zeichenfolge, sondern eine Zahl zurückgibt. Dann funktioniert der Verschluss bereits falsch, da sich die Funktion darin geändert hat, aber der Verschluss dies nicht erwartet, er hat sich nicht geändert. Und solche Probleme mit dem automatischen Laden können an unerwarteten Orten "schießen".



Wie werden Autorisierungsklassen aktualisiert? Sehr einfach. Es aktualisiert alle Methoden der Klasse auf die gleiche Weise wie Funktionen und aktualisiert auch das Attribut __class__ für alle Instanzen, sodass die Auflösung der Methoden (die bestimmt, welche Methode aufgerufen werden soll) auf neue Weise funktioniert.

In TheREPL ist alles etwas komplizierter, da sich beim Aktualisieren von _class_ möglicherweise einige Nachkommen, untergeordnete Klassen, herausstellen, die ebenfalls aktualisiert werden müssen, da sich in der Liste der Basisklassen etwas geändert hat.

Um dieses Problem zu lösen, können Sie die Klasse neu erstellen. Aber lassen Sie uns zuerst sehen, was beim automatischen Laden passiert, wenn ein Modul neu geladen wird.



Hier ist ein gutes Beispiel. Es gibt zwei Module - a und b. In Modul a wird eine übergeordnete Klasse definiert, in Modul b eine untergeordnete Klasse, und wir erstellen eine Instanz der untergeordneten Klasse. Und Zeile 10 zeigt, dass dies eine Instanz der Foo-Klasse ist, der übergeordneten.



Als nächstes nehmen und ändern wir einfach das Modul a. Fügen Sie beispielsweise der Foo-Klasse Dokumentation hinzu. Dann übernimmt das automatische Laden diese Änderungen. Was denkst du, dass er in diesem Fall von Bar zurückkehren wird?



Und es gibt false zurück, weil das automatische Laden die Foo-Klasse geändert hat und es sich nun um eine völlig andere Klasse handelt, nicht um die, von der die Bar-Klasse geerbt wird.



Und eine Überraschung! In den beiden Modulen a und b ist die Foo-Klasse eine andere Klasse, und Bar erbt von einer von ihnen. Aufgrund solcher Pfosten ist es sehr schwierig vorherzusagen, wie Ihr Code funktionieren wird, nachdem das automatische Laden etwas darin behoben hat.



So etwas aktualisiert Klassen. Ich werde das Bild kommentieren. Zunächst wird die Foo-Klasse in Modul b importiert und bleibt dort. Wenn Sie das automatische Laden ersetzen, wird dieses Modul a verschoben, und dort wird eine neue Klasse angezeigt. In Modul b wird es nicht aktualisiert.



TheREPL macht ein bisschen anders. Er fügt jedem Modul, in das er importiert wurde, eine modifizierte Klasse ein. Daher funktioniert dort alles richtig. Wenn sich Objekte in der Klasse befanden, bleiben diese erhalten.



Und so löst TheREPL das Problem mit untergeordneten Klassen. Das heißt, wenn sich die übergeordnete Klasse geändert hat, definiert sie die Liste der Basisklassen über das magische Attribut mro (Reihenfolge der Methodenauflösung). Dieses Attribut enthält eine Liste von Klassen in der Reihenfolge, in der Sie nach Methoden oder Attributen suchen möchten. Und jedes Mal, wenn Sie beispielsweise die Methode get_name für Ihr Objekt aufrufen, überprüft Python sie zuerst in der Bar-Klasse, dann in der Foo-Klasse und dann in der Objektklasse, wenn sie nicht gefunden wird. Es handelt sich um das Verfahren zur Reihenfolge der Methodenauflösung.

TheREPL verwendet diesen Chip. Es nimmt eine Liste von Basisklassen auf und ändert dort die Klasse, die Sie gerade geändert haben, in eine neue. Erstellt einen neuen untergeordneten Typ. Dies ist der zweite Schritt. Mit der Typfunktion können Sie tatsächlich Klassen erstellen. Wenn Sie es noch nie benutzt haben - probieren Sie es aus, es macht Spaß.

Sie sagen einfach den Namen der Klasse und die Basisklasse. Im einfachsten Fall zum Beispiel Objekt. Und - ein Wörterbuch mit Klassenmethoden und Attributen. Alles, Sie haben eine neue Klasse, die Sie wie gewohnt instanziieren können. TheREPL nutzt diesen Chip. Es generiert eine untergeordnete Klasse und ändert Zeiger darauf in allen Objekten der alten Bar-Klasse.

Ich habe noch eine Demo, schauen wir uns an, wie es funktioniert. Schauen wir uns zunächst eine so einfache Sache an.

Erste Demo

Ich sagte, dass Sie den Code innerhalb des Moduls ändern können. Angenommen, wir haben einen Server. Ich werde es jetzt ausführen. Irgendwann stellen wir fest, dass er aus irgendeinem Grund temporäre Verzeichnisse erstellt. Oder er fing an zu erschaffen, aber vorher schuf er nicht. Dann können wir eine Verbindung zu diesem Server herstellen. Wenn Sie davon ausgehen, dass diese Verzeichnisse wahrscheinlich mit der Funktion mkdtemp aus dem Dateimodul erstellt werden, können Sie direkt zu diesem Python-Modul wechseln.

Siehe - in der Ecke hat sich der Name des aktuellen Moduls geändert. Jetzt heißt es tempfile. Und ich kann sehen, welche Funktionen es gibt. Wir sehen sie und können sie vor allem neu definieren. Ich habe einen speziellen Wrapper vorbereitet, mit dem Sie jede Funktion so dekorieren können, dass Sie bei all ihren Aufrufen die Ablaufverfolgung sehen können, von der aus sie aufgerufen wird. Jetzt werden wir sie importieren und anwenden.

Das heißt, ich verpacke die Standard-Python-Funktion und habe nicht einmal Zugriff auf den Quellcode für dieses Modul. Ich kann es nehmen und einwickeln. Und bei der nächsten Ausgabe sehen wir Traceback und finden heraus, woher es aufgerufen wird.

Auf die gleiche Weise können diese Änderungen rückgängig gemacht werden, damit sie uns nicht als Spam versenden. Das heißt, wir sehen, dass dieser Server innerhalb des Workers in der achten Zeile mkdtemp aufruft und weiterhin temporäre Verzeichnisse für uns erstellt, wodurch das Dateisystem überfüllt wird. Dies ist eine Anwendung.

Schauen wir uns ein weiteres Beispiel an, warum das automatische Laden manchmal überhaupt nicht gut funktioniert. Ich habe einen Telegrammbot vorbereitet:

Zweite Demo

Jetzt aktivieren wir das automatische Laden und sehen, wie es uns hilft. Das war's, jetzt kannst du den Bot starten und mit ihm sprechen. Damit Sie besser sehen können, werden wir einen Dialog mit ihm beginnen. Lerne den Bot kennen. Damit. Es gibt einen Fehler. Ein völlig anderer Fehler wurde erfunden, und ich beschloss, im letzten Moment Änderungen vorzunehmen. Aber es spielt keine Rolle. Jetzt werden wir das Problem beheben. Das automatische Laden hilft uns dabei.

Wir wechseln zum Bot. Und jetzt werde ich dies vorübergehend kommentieren, wenn ja. Ich speichere die Datei. Theoretisch musste die Autoreload diese Änderungen erfassen. Starten Sie den Bot erneut. Der Bot hat mich erkannt. Reden wir mit ihm.

Ein weiterer Fehler. Sie ist bereits gezeugt. Lass es uns reparieren. Ich werde den Bot verlassen, es wird im Hintergrund funktionieren, ich werde zum Editor wechseln und im Editor werden wir diesen Fehler finden. Es ist nur ein Tippfehler und ich habe vergessen, dass meine Variable Benutzername heißt. Ich habe die Datei gespeichert. Autoreload sollte sie fangen, und jetzt werden wir es sehen.

Aber das automatische Laden, wie ich bereits erwähnt habe, weiß nichts darüber, dass sich die Datei geändert hat, bis Sie etwas eingeben. Mit so einem langen Prozess ... Es muss unterbrochen, neu gestartet werden. Erledigt. Geh zurück zu unserem Bot, schreib ihm. Sie sehen, der Bot hat vergessen, dass ich Sasha heiße. Warum? Beim automatischen Laden wurde es erneut erstellt, da das gesamte Modul vollständig neu geladen wird. Und ich muss erneut an den Bot schreiben, um seinen Zustand wiederherzustellen.

Und wenn Sie einen Fehler debuggen, der in einem bestimmten Zustand auftritt, kann der Zustand nicht verloren gehen, da Sie sonst erneut viel Zeit aufwenden, um diesen Zustand zu erreichen. TheREPL hilft in solchen Fällen.

Mal sehen, wie der Bot bei Verwendung von TheREPL aktualisiert wird. Für die Reinheit des Experiments werde ich IPython neu starten und wir werden es noch einmal wiederholen.

Und jetzt lade ich TheREPL herunter. Er beginnt sofort, einen bestimmten Port abzuhören, damit Sie einen Code darin senden können. Dies ist übrigens auch dann möglich, wenn IPython irgendwo auf dem Server ausgeführt wird und der Editor lokal ausgeführt wird, was in einigen Fällen auch hilfreich sein kann.

Wir importieren den Bot, starten ihn und schreiben erneut. Hier ist es klar - wir haben Python neu gestartet, daher kann es sich nicht erinnern, wer ich bin. Überprüfen Sie, ob ein Fehler vorliegt. Ja, es liegt ein Fehler vor. Nun, lass es uns erledigen.

Ich wechsle zurück zum Editor, korrigiere den Fehler. Wir müssen die Datei nicht einmal speichern. Ich drücke Strg-C, Strg-C. Dies ist eine Verknüpfung, mit der Emacs die aktuelle Beschreibung der Funktion direkt unter dem Cursor an den Python-Prozess sendet, mit dem sie verbunden ist. Das ist alles, jetzt können wir überprüfen, wie unser Bot dort auf meine Nachrichten reagiert. Jetzt erinnert er sich, dass ich Sasha bin und antwortet ehrlich, dass er nicht weiß wie.

Versuchen wir dort direkt neue Funktionen hinzuzufügen. Gehen Sie dazu zurück zum Editor. Fügen Sie beispielsweise den Befehl help hinzu. Lassen Sie ihn vorerst antworten, dass er nichts über Hilfe weiß. Drücken Sie erneut Strg-C, Strg-C, der Code wird angewendet. Wir gehen zum Bot. Sehen Sie, ob er diesen Befehl versteht. Ja, das Team hat sich beworben.

Übrigens hat er noch so etwas, jetzt schauen wir uns an, wie sich die Klasse verändern wird. Er hat einen Statusbefehl, einen speziellen Debugging-Befehl, um den Status des Bots anzuzeigen. Also haben sich einige Oleg verbunden. Interessant.

Wenn der Bot diesen Befehl ausführt, ruft er reply auf, um die Darstellung des Bots anzuzeigen. Wir können diese Antwort zum Beispiel mit etwas anderem korrigieren. Stellen Sie beispielsweise sicher, dass nur die Namen eingegeben werden. Sie können dies tun. Wir kehren zu unserem Boten zurück und führen den Zustand erneut aus. Und alle. Jetzt funktioniert die Antwort auf eine neue Art und Weise, aber das Objekt ist das gleiche, es hat seinen Zustand beibehalten, da es sich an uns alle erinnert - Oleg, Sasha, kek und "DROP TABLE Users, Alex"!

Auf diese Weise können Sie Code direkt im laufenden Betrieb schreiben und debuggen, ohne zu diesem Zyklus wechseln zu müssen. Wenn Sie ein Paket sammeln müssen, rollen Sie es irgendwo hin. Sie können schnell etwas testen, alles ändern, was Sie benötigen, und erst dann sollten alle diese Änderungen ordnungsgemäß verpackt und bereitgestellt werden.

Natürlich sollten Sie dies nicht in der realen Produktion tun, denn mit diesem Ansatz kann was für ein Problem sein. Möglicherweise vergessen Sie, dass der Code, den Sie gerade auf dem Server gestartet haben, gespeichert und dann ordnungsgemäß bereitgestellt werden muss. Dieser Ansatz erfordert Disziplin. Bei der Entwicklung und dem Debuggen von Tests ist dies jedoch eine großartige Sache.

Stellen Sie sicher, dass Sie ein Plugin für PyCharm erstellen. Wenn es einen Freiwilligen gibt, der mir mit Kotlin und dem PyCharm-Plugin hilft, werde ich gerne sprechen. Schreiben Sie mir per Post oder Telegramm .

* * *

Verbindenzur Entwicklung von TheREPL. Es gibt viel mehr Chips, an die Sie denken können. Sie können beispielsweise eine Möglichkeit finden, Klasseninstanzen beim Upgrade zu aktualisieren, dort neue Attribute hinzuzufügen oder ihren Status irgendwie zu aktualisieren. Ebenso werden wir die Datenbank aktualisieren. Nun ist das nicht.

Sie können einen Hot-Reload-Code für die Produktion erstellen, damit Sie den Server nicht neu starten müssen, wenn neue Änderungen zu Ihnen kommen. Sie können sich viel mehr einfallen lassen. Dies ist nur eine Idee, und ich möchte, dass Sie sie hier rausholen. Wir müssen alles für uns selbst anpassen und es bequem machen. Das ist alles für mich.

All Articles