无用的REPL。Yandex报告

REPL(read-eval-print循环)在Python中是无用的,即使它是神奇的IPython。今天,我将提供对此问题的可能解决方案之一。首先,该报告及其扩展名TheREPL对那些对更快,更高效的开发感兴趣的人以及编写有状态系统的人都非常有用。


-我叫Alexander,我是Yandex的程序员。我们正在使用Python编写团队,但尚未切换到Go。但奇怪的是,在空闲时间,我也用一种非常动态的语言-Common Lisp进行编程和执行。它可能比Python更动态。它的独特之处在于,开发过程本身的安排有些不同。它更具交互性和迭代性,因为在Lisp上的REPL中,您可以做所有事情:创建新模块和删除旧模块,添加方法,类并删除它们,重新定义类等。



在Python中,这更加困难。它具有IPython。当然,IPython以某种方式改进了REPL,增加了自动补全功能,并允许使用不同的扩展名。但是对于迭代开发,它并不十分适合。在其中,您可以下载代码,对其进行一些测试,仅此而已。有时他希望获得更多的交互性,以便您可以在开发中真正使用此REPL,在模块之间切换,更改模块中的功能和类。

它发生在我身上-例如,您在生产环境中运行IPython REPL,然后开始在该环境中运行一些命令,进行调查,然后发现该模块中存在错误,并且您想快速对其进行修复。但这是行不通的,因为您需要收集一个新的Docker映像,将其投入生产,再次进入该REPL,再次达到所需的状态,然后重新启动所有落在上面的东西。理想情况下,我必须修复该函数,立即运行它并立即获得结果。

关于这个还能做什么?如何在IPython中重新加载代码?我尝试使用自动重载,但由于某些原因,我不喜欢它。首先,重新启动模块后,它将失去该模块内部全局变量中的状态。并且可能存在具有某些功能结果的缓存值。或者,例如,我可以通过那里的网络加载数据,以便以后可以更快地使用它们。也就是说,自动重装会丢失状态。

因此,作为一个实验,我为IPython进行了简单的扩展并将其命名为TheREPL。

我是通过这份报告来到您的,是关于用Python中的REPL可以做什么的想法。我真的希望您会喜欢这个想法,将您的想法付诸实践,并将继续提出使Python更加高效和便捷的方法。

什么是REPL?这是您下载的扩展,之后在IPython中出现诸如名称空间之类的概念,您可以采用并切换到任何Python模块,查看其中包含哪些变量,函数等。更重要的是,您可以直接编写def(函数名称),重新定义函数或类,并且在导入它的所有模块中它都会更改。但是同时,模块本身不会重新启动,因此状态得以保存。另外,TheREPL允许您避免自动重载中的其他工件,现在我们将介绍这些工件。



因此,在自动重装中,代码升级仅在文件保存时进行。但是同时,您需要在REPL本身中输入一些内容,然后自动重载才能获取这些更改。这是问题编号1。也就是说,如果您在单独的线程中有某种后台进程(例如,服务器正在运行),则不能仅获取并更正代码。除非您在IPython REPL中输入内容,否则自动重载不会应用这些更改。

对于我的扩展程序,您在编辑器中向右按快捷方式,光标下的功能将立即应用并开始工作。也就是说,使用TheREPL,您可以更精细地更改代码。您也可以在IPython中编写def。



正如我所说,在模块之间切换不会自动支持任何方式。您只能在文件系统中找到该文件,对其进行更改,并希望自动重装能够解决其中的所有问题。



更远。 Autoreload丢失了全局变量,TheREPL保存并允许您继续研究应用程序的操作,更改其内部代码,从而快速开发它。



自动重新加载仍然具有此功能。他非常狡猾地将更改应用于重新加载的模块。特别是,他在那里做了一个非常有趣的把戏。如果此模块中的功能已更新,则为了在导入位置进行更改,他使用垃圾回收器来查找该功能以及所有这些功能实例,并更改其中的代码。此外,我们将看一下如何发生这种情况的示例。因此,即使功能代码进入了闭包,它也会发生变化。

你知道闭包是什么吗?这是非常有用的事情。 JavaScript开发人员一直在使用它。您,很可能也从未关注过。但是由于autoreload会执行我上面描述的操作,因此您可能会发现自己处于旧代码使用新代码而工作方式可能不同的情况。例如,一个函数不能返回一个值,而可以返回两个,元组而不是字符串,等等。旧代码将因此而中断。

REPL并没有做特别棘手的事情,以确保所有内容都更加一致。即,它更改了定义它的模块中的函数或类。在所有其他模块中找到此类,并在那里进行更改。在那之后,一切都以新的方式工作。



如何替换自动重装的功能?我们有两个功能,一个和两个。每个函数都有一组属性:文档,代码,参数等。幻灯片上的示例是替换存储字节码的属性的示例。

自动重载更改后,被调用函数开始以不同的方式工作。但这是一个人工合成的示例,我只是用手复制了一下,以便您了解正在发生的事情。该函数以一种方式调用,但是其中的代码实际上是不同的。而且,如果您进行反汇编,它也表明它返回了一个反演。这会导致什么?



这是关闭的示例。在第二行,我们创建一个闭包,其中捕获了函数foo。闭包本身希望我们传递的此函数返回一行,它将其以utf-8编码,然后一切正常。



但是,假设您更改了定义foo的模块,并且autoreload接受了更改。然后您对其进行更改,使其不返回字符串,而是一个数字。然后,该关闭将已经无法正常工作,因为其中的功能已在内部更改,但是关闭并不期望这样做,也没有更改。自动重载的此类问题可能会在意外的地方“解决”。



自动重载如何更新类?很简单。它以与函数相同的方式更新该类的所有方法,并且还更新所有实例的__class__属性,以便方法的解析(确定应调用哪个方法)以新的方式开始工作。

在TheREPL中,一切都有些复杂,因为更新_class_时,可能会发现它具有一些后代,即子类,它们也需要更新,因为基类列表中的某些内容已更改。

要解决此问题,您可以重建类。但是首先让我们看看自动重载在重载模块时会发生什么。



这是一个很好的例子。有两个模块-a和b。在模块a中,定义了一个父类,在模块b中定义了一个子类,我们创建了该子类的实例。第10行表明,是的,这是父Foo类的实例。



接下来,我们只需要更改模块a。例如,将文档添加到Foo类。然后,自动重新加载将获取这些更改。您认为他在这种情况下会从Bar回来吗?



它返回false,因为自动重载更改了Foo类,现在它是一个完全不同的类,而不是继承Bar类的类。



和一个惊喜!在两个模块a和b中,Foo类是一个不同的类,而Bar从其中一个继承。由于存在这种限制,很难在自动重载修复其中的某些内容后预测代码的工作方式。



像这样的东西,它会更新类。我将在图片上发表评论。最初,Foo类被导入到模块b中,因此仍保留在那里。替换自动重载时,此模块a会重新定位,并在那里出现一个新类,并且在模块b中不会对其进行更新。



REPL有所不同。他将修改后的类注入到导入他的每个模块中。因此,在那里一切正常。而且,如果类中有对象,则将保留它们。



这就是TheREPL如何解决子类的问题。也就是说,当父类已更改时,它将通过magic属性mro(方法解析顺序)定义基类列表。该属性包含您要在其中查找方法或属性的顺序的类列表。例如,每次您在对象上调用get_name方法时,Python都会先在Bar类中检查它,然后在Foo类中检查它,然后在对象类中检查它(如果找不到)。它按照方法解析顺序程序操作。

REPL使用此芯片。它需要一个基类列表,然后将您刚刚更改的类更改为新的基类。创建一个新的子类型,这是第二步。使用type函数,您实际上可以创建类。如果您从未使用过,请尝试一下,这很有趣。

您只需说出类的名称,说出它的基类是什么。在最简单的情况下,例如,对象。并且-具有类方法和属性的字典。像往常一样,所有内容都有一个可以实例化的新类。REPL利用了该芯片。它会生成一个子类,并在旧Bar类的所有对象中更改指向它的指针。

我还有一个演示,让我们看看它是如何工作的。首先,让我们看一下这样一个简单的事情。

第一个演示

我说过,您可以更改模块内部的代码。假设我们有一台服务器。我现在运行它。在某个时候,我们发现由于某种原因他创建了临时目录。或者他开始创造,但是在那之前他没有创造。然后,我们可以连接到该服务器,并猜测它可能使用文件模块中的mkdtemp函数创建了这些目录,您可以直接转到该Python模块。

请参阅-角落中的当前模块名称已更改。现在它说是tempfile。我可以看到有什么功能。我们看到了它们,并且重要的是,我们可以重新定义它们。我准备了一个特殊的包装器,可以包装任何函数,以便在调用所有函数时都可以从调用位置查看跟踪。现在,我们将导入并应用它们。

也就是说,我包装了标准的Python函数,甚至没有访问此模块的源代码的权限。我可以把它包起来。在下一个输出中,我们将看到Traceback并找到从何处调用它。

同样,可以回滚这些更改,以免对我们造成垃圾邮件。也就是说,我们看到位于第八行的worker内部的服务器调用mkdtemp并继续为我们生成临时目录,从而使文件系统混乱。这是一个应用程序。

让我们看一下为什么自动重新加载有时根本无法正常工作的另一个示例。我准备了一个电报机器人:

第二个演示

现在,我们激活自动重装,并查看它如何帮助我们。就是这样,现在您可以启动机器人并与他交谈。为了让您看得更好,我们将开始与他对话。了解机器人。所以。有某种错误。想到了一个完全不同的错误,我决定在最后一刻进行更改。但这没关系。现在,我们将对其进行修复,自动重装将对此提供帮助。

我们正在切换到机器人。现在,我将对此暂时发表评论。我保存文件。从理论上讲,自动重新加载必须抓住这些变化。再次启动机器人。机器人认出了我。让我们谈谈他。

另一个错误。她已经怀孕了。让我们去修复它。我将离开该机器人,它将在后台运行,切换到编辑器,然后在编辑器中发现此错误。这只是一个错字,我忘记了我的变量叫做user_name。我保存了文件。 autoreload应该可以抓住她的,现在我们将看到它。

但是,正如我已经提到的,自动重新加载对文件已更改的事实一无所知,除非您向其中输入内容。如此漫长的过程...需要中断,然后重新启动。做完了回到我们的机器人,给他写信。好吧,您看到了,该漫游器忘记了我的名字叫Sasha。为什么? autoreload再次重新创建了它,因为它完全重新加载了整个模块。而且我需要再次写入bot以恢复其状态。

而且,如果您要调试在特定状态下发生的某种错误,则该状态不会丢失,因为否则您将花费​​大量时间再次达到该状态。在这种情况下,REPL会提供帮助。

让我们看看在使用TheREPL的情况下如何更新机器人。为了使实验更纯净,我将重新启动IPython,然后将其重复一遍。

现在我下载TheREPL。他立即开始侦听特定端口,以便您可以在其中发送代码。顺便说一句,即使IPython在服务器上的某个位置运行并且编辑器在本地运行,也可以这样做,这在某些情况下也可以为您提供帮助。

我们导入机器人,启动它,然后再次编写。很清楚,这里-我们重新启动了Python,所以它不记得我是谁了。检查内部是否有错误。是的,有一个错误。好吧,让我们完成它。

我切换回编辑器,更正错误。我们什至不必保存文件,我按Ctrl-C,Ctrl-C,这是快捷方式,Emacs通过该快捷方式获取光标正下方的函数的当前描述,并将其发送到所连接的Python进程。就是这样,现在我们可以检查一下机器人如何在此处响应我的消息。现在,他记得我是Sasha,并且诚实地回答他不知道如何。

让我们尝试在此处直接添加新功能。为此,请返回编辑器。例如,添加帮助命令。现在,让他回答说,他对帮助一无所知。再次按Ctrl-C,Ctrl-C,将应用代码。我们去机器人。看看他是否理解此命令。是的,团队已经申请了。

顺便说一句,他仍然有这样的事情,现在我们来看一下班级将如何变化。他具有状态命令,这是一种特殊的调试命令,用于查看机器人的状态。因此,一些奥列格连接。有趣。

当机器人执行此命令时,它会调用Reply以查看机器人的表示形式。我们可以去更正,例如,用其他方式更正此答复。例如,使其仅输入名称。您可以这样做。我们回到信使,再次执行状态。就这样。现在,回复以一种新的方式工作,但是对象是相同的,它保留了状态,因为它记住了我们所有人-Oleg,Sasha,kek和“ DROP TABLE Users,Alex”!

因此,您可以直接编写和调试代码,而无需切换到该周期,当您需要收集软件包并将其滚动到某个地方时。您可以快速测试某些东西,更改所需的所有内容,然后才应将所有这些更改正确打包并部署。

自然,您不应该在实际生产中执行此操作,因为使用此方法可能会导致什么样的问题。您可能会忘记,刚刚保存在服务器上的代码需要保存,然后按原样部署。这种方法需要纪律。但是在某种测试的开发和调试过程中,这只是一件好事。

确保为PyCharm制作一个插件。如果有志愿人员帮助我使用Kotlin和PyCharm插件,我将很高兴与您交谈。用邮件电报给我写信

* * *

连接到TheREPL的发展。您可以想到更多芯片。例如,您可以想出一种在类实例升级时对其进行更新,在其中添加新属性或以某种方式升级其状态的方法。同样,我们将升级数据库。现在不是。

您可以提供用于生产的热重装代码,这样,当您进行新更改时,不必重新启动服务器。您可以提出更多建议。这只是一个想法,我希望您能从这里摆脱出来。我们必须为自己调整一切并使其方便。这就是我的全部。

All Articles