REPL inutile. Rapport Yandex

REPL (boucle read-eval-print) est inutile en Python, même s'il s'agit d'un IPython magique. Aujourd'hui, je proposerai l'une des solutions possibles à ce problème. Tout d'abord, le rapport et mon extension TheREPL seront utiles à ceux qui sont intéressés par un développement plus rapide et plus efficace, ainsi qu'à ceux qui écrivent des systèmes avec état.


- Je m'appelle Alexander, je travaille comme programmeur chez Yandex. Nous écrivons dans mon équipe en Python, nous ne sommes pas encore passés à Go. Mais pendant mon temps libre, je programme assez curieusement et je le fais dans un langage très dynamique - Common Lisp. Il est peut-être encore plus dynamique que Python. Sa particularité réside dans le fait que le processus de développement lui-même est organisé quelque peu différemment. Il est plus interactif et itératif, car dans REPL sur Lisp, vous pouvez tout faire: créer de nouveaux modules et les supprimer, ajouter des méthodes, des classes et les supprimer, redéfinir des classes, etc.



En Python, c'est d'autant plus difficile. Il a IPython. Bien sûr, IPython améliore REPL d'une certaine manière, ajoute la saisie semi-automatique et permet d'utiliser différentes extensions. Mais pour le développement itératif, cela ne convient pas très bien. Vous pouvez y télécharger le code, le tester un peu et c'est tout. Et parfois, il veut plus d'interactivité pour que vous puissiez vraiment utiliser ce REPL dans le développement, basculer entre les modules, changer les fonctions et les classes à l'intérieur.

Cela m'arrive - vous exécutez, par exemple, IPython REPL dans l'environnement de production et vous commencez à exécuter des commandes là-bas, à enquêter sur quelque chose, puis il s'avère qu'il y a une erreur dans le module et que vous souhaitez la corriger rapidement. Mais cela ne fonctionne pas, car vous devez créer une nouvelle image Docker, la rouler en production, recommencer dans ce REPL, atteindre à nouveau l'état souhaité, redémarrer tout ce qui est tombé dessus. Et idéalement, je devrais corriger la fonction, l'exécuter immédiatement et obtenir instantanément le résultat.

Que peut-on faire à ce sujet? Comment puis-je recharger du code dans IPython? J'ai essayé d'utiliser le chargement automatique et je ne l'aimais pas pour plusieurs raisons. Tout d'abord, lorsque le module est redémarré, il perd l'état qui était dans les variables globales à l'intérieur de ce module. Et il peut y avoir une valeur en cache avec les résultats de certaines fonctions. Ou je pourrais, par exemple, y charger des données sur le réseau, afin que plus tard, je puisse travailler avec elles plus rapidement. Autrement dit, le chargement automatique perd son état.

Par conséquent, à titre expérimental, j'ai créé mon extension simple pour IPython et l'ai nommée TheREPL.

Je suis venu vers vous avec ce rapport comme une idée de ce qui peut être fait avec REPL en Python. Et j'espère vraiment que vous aimerez cette idée, que vous la réaliserez dans votre tête et que vous continuerez à proposer des choses qui rendront Python encore plus efficace et pratique.

Qu'est-ce que TheREPL? Il s'agit de l'extension que vous téléchargez, après laquelle un concept tel que l'espace de noms apparaît dans IPython, et vous pouvez prendre et basculer vers n'importe quel module Python, voir quelles variables, fonctions, etc. sont là. Et plus important encore, vous pouvez écrire directement def, le nom de la fonction, redéfinir la fonction ou la classe, et cela changera dans tous les modules où elle a été importée. Mais en même temps, le module lui-même ne redémarre pas, donc l'état est enregistré. De plus, TheREPL vous permet d'éviter d'autres artefacts qui sont en chargement automatique et que nous allons maintenant examiner.



Ainsi, en chargement automatique, la mise à niveau du code ne se produit que lorsque le fichier est enregistré. Mais en même temps, vous devez entrer quelque chose dans le REPL lui-même, et ce n'est qu'alors que le chargement automatique reprendra ces modifications. C'est le problème numéro 1. Autrement dit, si vous avez une sorte de processus d'arrière-plan dans un thread séparé (par exemple, le serveur est en cours d'exécution), vous ne pouvez pas simplement prendre et corriger le code. Le chargement automatique n'appliquera pas ces modifications tant que vous n'aurez pas entré quelque chose dans IPython REPL.

Dans le cas de mon extension, vous appuyez sur le raccourci droit dans l'éditeur, et la fonction qui se trouve sous le curseur est immédiatement appliquée et commence à fonctionner. Autrement dit, en utilisant TheREPL, vous pouvez modifier le code de manière plus granulaire. Vous pouvez également écrire def dans IPython.



Comme je l'ai dit, passer d'un module à l'autre, le chargement automatique ne prend aucunement en charge. Vous pouvez uniquement trouver le fichier dans le système de fichiers, le changer et espérer que le chargement automatique résoudra tout là-bas.



Plus loin. Le chargement automatique perd les variables globales, TheREPL enregistre et vous permet de continuer à rechercher le fonctionnement de votre application, changer son code interne et ainsi le développer rapidement.



Le chargement automatique a toujours cette fonctionnalité. Il applique très astucieusement les modifications au module qui se recharge. En particulier, il y fait un tour très intéressant. Si la fonction dans ce module a été mise à jour, alors pour la changer où qu'elle soit importée, il utilise le garbage collector pour la trouver et toutes ces instances de fonctions et changer le code qu'elles contiennent. De plus, nous examinerons des exemples de la façon dont cela se produit. Pour cette raison, le code de fonction change, même s'il entre dans la fermeture.

Savez-vous ce qu'est une fermeture? C'est une chose très utile. Les développeurs JavaScript l'utilisent tout le temps. Vous, très probablement, n'avez tout simplement jamais prêté attention. Mais comme le chargement automatique fait ce que j'ai décrit ci-dessus, vous pouvez vous retrouver dans une situation où l'ancien code utilise un nouveau code qui peut fonctionner différemment. Par exemple, une fonction peut renvoyer non pas une valeur, mais deux, tuple au lieu de chaîne, etc. L'ancien code se cassera à ce sujet.

Le REPL ne fait pas une chose aussi délicate spécifiquement pour s'assurer que tout est plus cohérent. En d'autres termes, il modifie la fonction ou la classe du module dans lequel il est défini. Recherche cette classe dans tous les autres modules et la modifie également là-bas. Après cela, tout fonctionne d'une manière nouvelle.



Comment fonctionne le remplacement de la fonction de chargement automatique? Nous avons deux fonctions, une et deux. Chaque fonction possède un ensemble d'attributs: documentation, code, arguments, etc. Ici sur la diapositive est un exemple de remplacement des attributs dans lesquels le bytecode est stocké.

Une fois le chargement automatique modifié, la fonction appelée commence à fonctionner différemment. Mais c'est un exemple synthétique que je viens de reproduire avec mes mains pour que vous compreniez ce qui se passe. La fonction est appelée dans un sens, mais le code est en fait différent. Et si vous démontez, cela montre également qu'il retourne un diable. À quoi cela mène-t-il?



Voici un exemple de fermeture. Sur la deuxième ligne, nous créons une fermeture dans laquelle nous capturons la fonction foo. La fermeture elle-même attend que cette fonction que nous avons passée retourne une ligne, elle l'encode en utf-8 et tout fonctionne.



Mais supposons que vous modifiez le module dans lequel foo est défini et que le chargement automatique reprend le changement. Et vous le modifiez pour qu'il ne renvoie pas une chaîne, mais un nombre. Ensuite, la fermeture fonctionnera déjà incorrectement, car la fonction qu'elle contient a changé à l'intérieur, mais la fermeture ne s'y attend pas, elle n'a pas changé. Et de tels problèmes avec le chargement automatique peuvent "tirer" dans des endroits inattendus.



Comment le chargement automatique met-il à jour les classes? Très simple. Il met à jour toutes les méthodes de la classe de la même manière que les fonctions, et il met également à jour l'attribut __class__ pour toutes les instances afin que la résolution des méthodes (déterminant la méthode à appeler) commence à fonctionner d'une nouvelle manière.

Tout est un peu plus compliqué dans TheREPL, car lorsque vous mettez à jour _class_, il peut s'avérer qu'il a des descendants, des classes enfants, qui doivent également être mises à jour, car quelque chose a changé dans la liste des classes de base.

Pour résoudre ce problème, vous pouvez reconstruire la classe. Mais voyons d'abord ce qui se passe avec le chargement automatique lorsqu'il recharge un module.



Voici un bon exemple. Il y a deux modules - a et b. Dans le module a, une classe parent est définie, dans le module b une classe enfant, et nous créons une instance de la classe enfant. Et la ligne 10 montre que oui, c'est une instance de la classe Foo, le parent.



Ensuite, nous prenons et modifions simplement le module a. Par exemple, ajoutez de la documentation à la classe Foo. Le chargement automatique reprend ensuite ces modifications. Que pensez-vous que dans ce cas, il reviendra de Bar?



Et il renvoie false, car le chargement automatique a changé la classe Foo, et maintenant c'est une classe complètement différente, pas celle dont la classe Bar est héritée.



Et une surprise! Dans les deux modules a et b, la classe Foo est une classe différente et Bar hérite de l'un d'eux. En raison de ces montants, il est très difficile de prédire comment votre code fonctionnera après que le chargement automatique aura corrigé quelque chose.



Quelque chose comme ça, il met à jour les classes. Je commenterai la photo. Initialement, la classe Foo est importée dans le module b, et elle y reste donc. Lors du remplacement du chargement automatique, ce module a est déplacé et une nouvelle classe y apparaît, et dans le module b, il n'est pas mis à jour.



TheREPL fait un peu différent. Il injecte une classe modifiée dans chaque module où il a été importé. Par conséquent, tout fonctionne correctement là-bas. De plus, s'il y avait des objets dans la classe, ils seront conservés.



Et c'est ainsi que TheREPL résout le problème des classes enfants. Autrement dit, lorsque la classe parente a changé, elle définit la liste des classes de base via l'attribut magique mro (ordre de résolution de la méthode). Cet attribut contient une liste de classes dans l'ordre dans lequel vous souhaitez rechercher des méthodes ou des attributs dans celles-ci. Et chaque fois que vous appelez la méthode get_name sur votre objet, par exemple, Python le vérifiera d'abord dans la classe Bar, puis dans la classe Foo, puis dans la classe objet, s'il ne le trouve pas. Il agit selon la procédure d'ordre de résolution de la méthode.

TheREPL utilise cette puce. Il prend une liste de classes de base, y change la classe que vous venez de changer en une nouvelle. Crée un nouveau type enfant, c'est la deuxième étape. Avec la fonction type, vous pouvez réellement créer des classes. Si vous ne l'avez jamais utilisé - essayez-le, c'est amusant.

Vous dites simplement le nom de la classe, dites quelle est sa classe de base. Dans le cas le plus simple, par exemple, objet. Et - un dictionnaire avec des méthodes et des attributs de classe. Tout, vous avez une nouvelle classe que vous pouvez instancier, comme d'habitude. TheREPL profite de cette puce. Il génère une classe enfant et y change des pointeurs dans tous les objets de l'ancienne classe Bar.

J'ai encore une démo, regardons comment ça marche. Tout d'abord, regardons une chose aussi simple.

Première démo

J'ai dit que vous pouvez changer le code à l'intérieur du module. Supposons que nous ayons un serveur. Je vais l'exécuter maintenant. À un moment donné, nous constatons que pour une raison quelconque, il crée des répertoires temporaires. Ou il a commencé à créer, mais avant cela, il n'a pas créé. Ensuite, nous pouvons nous connecter à ce serveur et, en supposant qu'il crée probablement ces répertoires en utilisant la fonction mkdtemp du module de fichiers, vous pouvez accéder directement à ce module Python.

Voir - dans le coin le nom du module actuel a changé. Maintenant, il dit tempfile. Et je peux voir quelles sont les fonctionnalités. Nous les voyons et nous pouvons, surtout, les redéfinir. J'ai préparé un wrapper spécial qui vous permet de décorer n'importe quelle fonction afin que, avec tous ses appels, vous puissiez voir la trace d'où elle est appelée. Nous allons maintenant les importer et les appliquer.

Autrement dit, j'encapsule la fonction Python standard, sans même avoir accès au code source de ce module. Je peux le prendre et l'envelopper. Et à la sortie suivante, nous verrons Traceback et trouver d'où il est appelé.

De la même manière, ces modifications peuvent être annulées afin qu'elles ne nous spamment pas. Autrement dit, nous voyons que ce serveur à l'intérieur du travailleur sur la huitième ligne appelle mkdtemp et continue de produire des répertoires temporaires pour nous, encombrant le système de fichiers. Ceci est une application.

Voyons un autre exemple de la raison pour laquelle le chargement automatique ne fonctionne parfois pas très bien du tout. J'ai un bot de télégramme préparé:

Deuxième démo

Maintenant, nous activons le chargement automatique et voyons comment cela nous aide. Voilà, maintenant vous pouvez démarrer le bot et lui parler. Pour que vous puissiez mieux voir, nous entamerons un dialogue avec lui. Apprenez à connaître le bot. Donc. Il y a une sorte d'erreur. Une erreur complètement différente a été conçue et j'ai décidé de faire des changements au dernier moment. Mais cela n'a pas d'importance. Maintenant, nous allons le corriger, le chargement automatique nous aidera avec cela.

Nous passons au bot. Et maintenant, je vais commenter temporairement cela, si c'est le cas. J'enregistre le fichier. le chargement automatique, en théorie, devait intercepter ces changements. Redémarrez le bot. Le bot m'a reconnu. Parlons-lui.

Encore une erreur. Elle est déjà conçue. Allons le réparer. Je vais quitter le bot, cela fonctionnera en arrière-plan, je passerai à l'éditeur, et dans l'éditeur, nous trouverons cette erreur. C'est juste une faute de frappe, et j'ai oublié que ma variable s'appelle nom_utilisateur. J'ai enregistré le fichier. le chargement automatique était censé l'attraper, et maintenant nous le verrons.

Mais le chargement automatique, comme je l'ai déjà mentionné, ne sait rien du fait que le fichier a changé jusqu'à ce que vous y entriez quelque chose. Avec un processus aussi long ... Il doit être interrompu, redémarré. Terminé. Retournez à notre robot, écrivez-lui. Eh bien, vous voyez, le bot a oublié que je m'appelle Sasha. Pourquoi? autoreload l'a recréé à nouveau car il recharge complètement le module entier. Et j'ai besoin d'écrire à nouveau au bot pour restaurer son état.

Et si vous déboguez une sorte d'erreur qui se produit dans un certain état, alors l'état ne peut pas être perdu, car sinon vous passerez encore beaucoup de temps pour atteindre cet état. TheREPL aide dans de tels cas.

Voyons comment le bot sera mis à jour en cas d'utilisation de TheREPL. Pour la pureté de l'expérience, je redémarrerai IPython et nous le répéterons encore une fois.

Et maintenant je télécharge TheREPL. Il commence immédiatement à écouter sur un port spécifique afin que vous puissiez envoyer un code à l'intérieur. Soit dit en passant, cela peut être fait même si IPython s'exécute quelque part sur le serveur et que l'éditeur s'exécute localement, ce qui peut également vous aider dans certains cas.

Nous importons le bot, le démarrons, écrivons à nouveau. C'est clair ici - nous avons redémarré Python, donc il ne se souvient plus de qui je suis. Vérifiez qu'il y a une erreur à l'intérieur. Oui, il y a une erreur. Eh bien, faisons-le.

Je reviens à l'éditeur, corrige l'erreur. Nous n'avons même pas besoin d'enregistrer le fichier, j'appuie sur Ctrl-C, Ctrl-C, c'est un raccourci par lequel Emacs prend la description actuelle de la fonction qui se trouve juste sous le curseur et l'envoie au processus Python auquel il est connecté. C'est tout, maintenant nous pouvons passer en revue et vérifier comment notre bot répond à mes messages là-bas. Maintenant, il se souvient que je suis Sasha et répond honnêtement qu'il ne sait pas comment.

Essayons d'y ajouter directement de nouvelles fonctionnalités. Pour ce faire, revenez à l'éditeur. Par exemple, ajoutez la commande help. Pour l'instant, laissez-le répondre qu'il ne sait rien de l'aide. Encore une fois, appuyez sur Ctrl-C, Ctrl-C, le code est appliqué. Nous allons au bot. Voyez s'il comprend cette commande. Oui, l'équipe a postulé.

Soit dit en passant, il a encore une telle chose, maintenant nous allons voir comment la classe va changer. Il a une commande d'état, une commande de débogage spéciale pour afficher l'état du bot. Donc, certains Oleg connectés. Intéressant.

Lorsque le bot exécute cette commande, il appelle la réponse pour afficher la représentation du bot. Nous pouvons aller corriger, par exemple, cette réponse avec autre chose. Par exemple, faites en sorte que seuls les noms soient entrés. Vous pouvez le faire. Nous retournons à notre messager, exécutons à nouveau l'état. Et c'est tout. Maintenant, la réponse fonctionne d'une manière nouvelle, mais l'objet est le même, il a conservé son état, car il se souvient de nous tous - Oleg, Sasha, kek et «DROP TABLE Users, Alex»!

Ainsi, vous pouvez écrire et déboguer du code directement à la volée, sans passer à ce cycle, lorsque vous devez collecter un package, le faire rouler quelque part. Vous pouvez rapidement tester quelque chose, changer tout ce dont vous avez besoin, et alors seulement toutes ces modifications doivent être correctement packagées et déployées.

Naturellement, vous ne devriez pas faire cela en production réelle, car avec cette approche, quel type de problème peut être. Vous pouvez oublier que le code que vous venez de démarrer sur le serveur doit être enregistré puis déployé comme il se doit. Cette approche nécessite de la discipline. Mais dans le processus de développement et de débogage d'une sorte de test, c'est juste une bonne chose.

Assurez-vous de créer un plugin pour PyCharm. S'il y a un volontaire qui m'aidera avec Kotlin et le plugin PyCharm, je serai heureux de parler. Écrivez-moi par la poste ou par télégramme .

* * *

Se connecterau développement de TheREPL. Il y a beaucoup plus de puces auxquelles vous pouvez penser. Par exemple, vous pouvez trouver un moyen de mettre à jour les instances de classe lors de leur mise à niveau, d'y ajouter de nouveaux attributs ou de mettre à niveau leur état d'une manière ou d'une autre. De même, nous mettrons à niveau la base de données. Maintenant ce n'est pas le cas.

Vous pouvez trouver un code de rechargement à chaud pour la production afin que lorsque de nouvelles modifications vous parviennent, vous n'avez pas à redémarrer le serveur. Vous pouvez trouver beaucoup plus. Ce n'est qu'une idée, et je veux que vous la retiriez d'ici. Nous devons tout régler pour nous-mêmes et le rendre pratique. C'est tout pour moi.

All Articles