pythonist的宏。Yandex报告

如何扩展Python语法并为其添加必要的功能?去年夏天,我在PyCon上试图阐明这个话题。从该报告中,您可以了解pytest,macropy,模式库的排列方式以及它们如何获得如此有趣的结果。最后,有一个使用HyLang(一种在Python之上运行的类似Lisp的语言)中的宏使用宏生成代码的示例。


- 嗨,大家好。首先,我要感谢PyCon的组织者。我是Yandex的开发人员。该报告根本不是关于工作,而是关于实验性的事情。也许他们会让你们中的一个人想到,在Python中您可以做一些您以前从未知道过,没朝这个方向思考的很棒的事情。

对于那些不知道宏是什么的人来说,这有点:当这种语言中的某些表达式扩展为更复杂的代码时,这就是这种代码生成方法。对您有什么好处?对您来说,宏记录是简洁的,它表达了一些抽象,但是它为您做了很多工作,并且您不需要用手编写所有这些代码。

pytest


您很有可能遇到了pytest测试框架,几乎可以肯定这里使用了它。我不知道你是否曾经注意到,但是在幕后,他也做了一些魔术。



例如,您有一个简单的测试。如果您在没有pytest的情况下运行它,那么它将简单地引发AssertionError。



不幸的是,我的例子有点退化,在这里很明显len是从三个元素的列表中获取的。但是,如果调用了某个函数,那么您将永远不会从这样的AssertionError中知道该函数返回了。她退还的东西不等于一百。



但是,如果此程序在pytest下运行,则它将显示其他调试信息。他在里面怎么做?



这个魔术非常简单。 Pytest创建了自己的特殊钩子,当带有测试的模块加载时会触发。此后,pytest独立地解析此Python文件,并且作为解析的结果,获得了其中间表示形式,称为AST树。 AST树是一个基本概念,可让您即时更改Python代码。

接收到这样的树后,pytest对其进行强制转换,以查找所有称为assert的表达式。他以某种方式对其进行了更改,他编译了新的AST树,并获得了带有测试的模块,该模块随后在常规的Python虚拟机上运行。



这就是未转换为pytest的原始AST树的样子。突出显示的红色区域是我们的断言。如果仔细观察,您将看到它的左右部分,即列表本身。

当pytest转换并生成新的一年时,树开始看起来像这样。



pytest为您生成了大约一百行代码。



如果将此AST树转换回Python,它将看起来像这样。pytest在此处以红色突出显示的区域是pytest计算表达式左右部分,生成错误消息并在此错误消息出问题的情况下引发AssertionError的地方。

模式匹配


这样的事情你还能做什么?您可以转换任何Python代码。而且我在PyPI上偶然发现了一个很棒的库,在那儿挖掘很有趣。她进行模式匹配。



也许这段代码是某人熟悉的。他递归地考虑阶乘。让我们看看如何使用模式匹配将其记录下来。



为此,只需将装饰器挂在函数上即可。请注意:在体内,功能已经有所不同。这些if中的每一个都是用于模式匹配的规则,该规则解析输入到函数的表达式并以某种方式对其进行转换。而且,结果甚至没有明确的返回。因为模式库在转换函数主体时首先会检查它是否包含,其次才添加结果的隐式返回,从而改变语言的语义。也就是说,她制作了一个新的DSL,其工作方式略有不同。有了这个,您可以声明性地写下一些东西。


前一个功能好像写成三行。





并且其余各行添加了其他功能,例如,允许从值列表中读取阶乘或将其传递给任意函数。

如何自己编写转化?宏!


现在您可能想知道,但是如何自己应用呢?因为这样做很麻烦,例如pytest:手动解析文件,所以需要查找需要转换的代码。在pytest中,一千个或更多行的单独模块执行此操作。

为了不自己做,一些聪明的人已经为我们想出了一个名为macropy的模块。

该模块的版本适用于第二个Python和第三个Python。他们在第二个Python时代将其写回。然后,这两个家伙开了个玩笑,以弄清楚Python可以做什么,并且库中包含各种示例。让我们看一下它们,它们将使您了解使用此技术可以做什么。他们在本教程中描述的第一个很酷的东西是一个宏,该宏为第二个Python实现格式字符串,如第三个一样。



用红色突出显示的表达式只是宏调用的语法。字母S是宏的名称,然后在方括号中是它转换的表达式。结果,变量在这里被替换。这在第二个Python中有效,但是在这样的宏中不再需要第三个。因此,例如,您可以制作自己的宏,该宏实现比标准格式字符串更复杂的语义和更有趣的事情。



当宏扩展时,并且这种情况发生在加载模块时,它只是转换为该代码。将占位符插入格式字符串,并对其应用替换过程。进一步的Python已经以一种标准的方式来编译所有这些。在运行时,不会发生宏扩展。所有这些都在模块加载时发生。因此,在这种情况下,您甚至可以进行优化或计算,这些优化或计算将在加载模块时进行,并生成更理想的字节码。



第二个例子也很有趣。这是编写lambda的简写形式。宏f接受一系列参数,并返回一个函数。每个以宏名“ f”开头的表达式,方括号,然后绝对将任何表达式转换为lambda。



在我看来,这也很酷,特别是对于那些喜欢以功能风格开发和编写代码并使用MapReduce的人而言。


这是另一个熟悉的示例。此功能认为是阶乘,代码以红色突出显示。她接到电话后会怎样?



它将在Python中引发错误,因为它将遇到堆栈限制,并且将出现难看的RecursionError。



如何解决?使用宏,解决问题非常简单。



您挂起装饰器,它将获取函数的主体并以某种神奇的方式对其进行转换。您不需要更改函数本身的任何内容,macropy会为您做所有事情。



并且该功能将返回到很正常的结果,并深入到地下。


它的宏观表现如何?



它用一个特殊的TailCall对象替换了对该函数本身的所有调用,然后由TCO装饰器在循环中调用该对象。



电路看起来像这样。循环中的装饰器将调用该函数,直到返回正常结果而不是TailCall为止。如果她回来了,那就把它退回来。就这样。这些很酷的事情可以用宏来完成!

Macropy还包括其他示例。我希望那些对您感到好奇的人自己去看看。假设有些东西对调试有用。



我会告诉你另一件事。这个查询宏就是一个例子。他在做什么?在其中编写常规的Python代码,然后将其用作执行此表达式的常规结果。但是在内部,macropy会将此代码转换为Alchemy SQL查询语言代码。



他为您重写了它,使这个可怕的表情。可以用手重写它,然后它会更短。我做的。



这是原始表达。扩展宏后,它具有类似的功能。



也许有人对编写更类似于Python的代码感兴趣,而不是强迫其开发人员在DSL SQL Alchemy上编写查询。

以相同的方式,您可以从Python生成任何东西-纯SQL,JavaScript-并将其保存在文件旁边的某个位置,然后在前端使用它。



现在,让我们看看如何制作自己的宏。使用macropy,这非常简单。

宏是一种将AST树作为输入的函数,并通过某种方式对其进行转换,从而返回一个新的树。这是一个宏示例,该示例向包含源表达式的assert调用添加了描述,以便我们可以理解为何发生AssertionError错误。

在这里,内部replace_assert函数是帮助器。她为您在树上进行递归下降。在replace_assert内部,传递了子树元素。



因此,您可以在内部检查其类型以及?如果是Assert调用,请对其进行处理。在这里,我将给出一个简单的综合示例,该示例采用左侧部分,右侧部分,从它们中产生错误消息,并将所有内容写入msg属性。这是需要返回的消息。







使用它时,您可以使用with上下文管理器将这样的宏附加到代码块上,并且进入上下文管理器的所有代码都将经过此转换。从下面可以看出,我们的错误消息已添加到AssertionError中,该错误消息是由len表达式([1,2,3])形成的。



但是,这种方法有一个局限性,使我个人感到难过。我尝试做一个实验,以设计出适用于该语言的新设计。例如,有些人喜欢开关或条件构造,除非。但是不幸的是,这是不可能的:当已经读取源代码并将其分解为令牌时,将使用macropy和其他与AST树配合使用的工具。该代码由Python解析器读取,其语法在解释器中固定。要更改它,您需要重新编译Python。当然,您可以执行此操作,但是它已经是Python的分支,而不是可以在PyPI上进行布局的库。因此,不可能使用宏来进行这种构造。

海朗


幸运的是,在我的一生中,我不仅用Python编写,而且对其他各种替代语言也很感兴趣。有许多人不喜欢的语法,但是更简单灵活。这些是s表达式。

对我们来说幸运的是,有一个名为HyLang的Python插件。这个东西有点让人想起Clojure,只有Clojure运行在JVM之上,而HyLang运行在Python虚拟机之上。也就是说,它为您提供了用于编写代码的新语法。但是同时,您编写的所有代码将与现有的Python库完全兼容,并且可以在Python库中使用。



看起来像这样。



左边的部分用Python写,右边的部分-HyLang。从底部到底部都是字节码,这就是结果。您可能已经注意到它是完全一样的,只是语法有所变化。 HyLang s表达式,很多人都不喜欢。反对者不理解这种语法赋予了语言巨大的力量,因为它使语言结构统一。统一性使您可以使用宏来实现任何设计。

之所以能够做到这一点,是因为在每个表达式中第一个元素始终是某种动作。然后他的论点去了。

而且所有代码都是由嵌套表达式组成的,这些表达式易于转换并在其中打开宏。因此,绝对可以在HyLang中进行任何新的构造,并且与代码中的标准语言功能没有任何区别。



让我们看看一个简单的宏如何在HyLang上工作。要执行与使用Macropy的Assert相同的操作,只需此代码。

我们的HyLang宏接收输入,即代码。此外,宏可以轻松使用此代码的任何部分来创建新代码。宏和函数之间的主要区别:表达式是输入,而不是值。如果我们将宏称为(is(= 1 2)),则它将收到一个表达式(= 1 2)而不是False。



因此,我们可以生成错误消息,说明出现了问题。



然后只需返回新代码。反引号和波浪号语法的含义类似于以下内容。反引号说:将此表达式照原样返回原样。波浪号表示:在此替换变量的值。



因此,当我们编写此代码时,宏在扩展时将返回给我们一个新的表达式,从而将其与其他错误消息一起声明。

HyLang很酷。是的,虽然我们不使用它。也许我们永远不会。所有这些项目都是实验性的。我希望您离开这里,感觉是在Python中您可以做一些您以前可能没有想到的事情。也许其中一些会在您正在进行的工作中找到实际的应用。

这就是我的全部。您可以看到以下链接:

  • 模式
  • MacroPy
  • HyLang
  • 本书OnLisp-对宏功能的高级研究。这是针对那些特别感兴趣的人。是的,这本书并不完全基于Python,而是基于Common Lisp。但是对于更深入的研究,这甚至会很有趣。

All Articles