PEP 638-Python的类似Lisp的语法宏

2020-10-14 18:57:32

这个PEP向Python添加了对语法宏的支持。宏是一个编译时函数,它转换程序的一部分,以允许在正常库代码中无法清晰表达的功能。

术语语法是指这种类型的宏在程序语法树上操作。这降低了基于文本的替换宏可能发生的误翻译的机会,并允许实现卫生宏[1]。

语法宏允许库在编译期间修改抽象语法树,提供了为特定领域扩展语言的能力,而不会增加整个语言的复杂性。

新的语言特性可能是有争议的,颠覆性的,有时甚至是分裂的。Python现在已经足够强大和复杂,许多建议的添加都是由于额外的复杂性而对语言造成的净损失。

虽然语言更改可能会使某些模式易于表达,但这是有代价的。每一个新功能都会使语言变得更大、更难学、更难理解。Python曾被描述为适合你的大脑[2],但随着添加的功能越来越多,这种说法变得越来越不正确。

因为添加新功能的成本很高,所以很难或不可能添加一个只会让一些用户受益的功能,而不管有多少用户,或者该功能会给他们带来多大的好处。

在过去的几年里,Python在数据科学和机器学习中的应用增长非常迅速,但是大多数Python的核心开发人员都没有数据科学或机器学习的背景,这使得核心开发人员很难确定机器学习的语言扩展是否值得。

通过允许语言扩展像库一样是模块化的和可分发的,特定于领域的扩展可以被实现,而不会对域外的用户产生负面影响。Web开发人员可能希望从数据科学家那里获得一组截然不同的扩展。我们需要让社区开发他们自己的扩展。

如果没有某种形式的用户定义的语言扩展,那些想要保持语言紧凑和适合他们大脑的人和那些想要适合他们领域或编程风格的新功能的人之间将会有一场持续的战斗。

许多领域看到重复的模式,这些模式很难或不可能表示为一个库。宏可以允许以更简洁、更不容易出错的方式来表示这些模式。

可以使用宏来演示潜在的语言扩展。例如,宏可以使WITH语句和YILD FROM表达式经过测试。这样做很可能会在第一个版本中通过允许在语言中包含这些功能之前进行更多测试来实现更高质量的实现。

在新功能发布之前几乎不可能确保它是完全可靠的;与功能的使用和收益相关的错误在发布很多年后仍在修复中。

历史上,新的语言特性都是通过天真地将AST编译成新的、复杂的字节码指令来实现的,这些字节码通常有自己的内部流程控制,执行那些可以也应该在编译器中完成的操作。

例如,直到最近,try-inal和with语句中的流控制都是由具有上下文相关语义的复杂字节码管理的,而这些语句中的控制流现在是在编译器中实现的,这使得解释器变得更简单、更快。

通过将新特性实现为AST转换,现有编译器可以为特性生成字节码,而不必修改解释器。

如果我们要提高CPythonVM的性能和可移植性,就需要一个稳定的解释器。

Python既有表现力,又易于学习;它被广泛认为是最容易学习、被广泛使用的编程语言,但它并不是最灵活的。那个标题属于LISP。

因为LISP是同形的,也就是说LISP程序是LISP数据结构,所以LISP程序可以由LISP程序操作,因此LISP的大部分语言都可以自己定义。

我们希望在Python中有这样的能力,而不是像lis语言那样有很多括号。幸运的是,一种语言不需要同象来操纵自己,所需要的只是在解析之后、但在转换成可执行形式之前操纵程序的能力。

Python已经有了所需的组件,Python的语法树可以通过ast模块获得,所需要的只是一个告诉编译器宏存在的标记,以及编译器回调到用户代码以操作AST的能力。

任何标识符字符序列后跟感叹号(感叹号,英国英语)都将被标记为MACRO_NAME。

宏_stmt=宏名称测试列表[";导入";名称][";作为";名称][";:";换行套件]。

宏的语句形式优先,因此代码MACRO_NAME!(X)将被解析为宏语句,而不是包含宏表达式的表达式语句。

在转换为字节码的过程中遇到宏时,代码生成器将查找为该宏注册的宏处理器,并将以该宏为根的AST传递给处理器函数。然后,返回的AST将被替换为原始树。

对于具有多个名称的宏,会将几个树传递给宏处理器,但只会返回并替换一个树,从而缩短了封闭的语句块。

在到达宏之前,编译器不会查找宏处理器,因此内部宏不需要注册处理器。例如,在切换宏中,CASE和DEFAULT宏将需要注册处理器,因为它们将被切换处理器清除。

要启用要导入的宏定义,请导入宏!而且是从那里来的!是预定义的。它们支持以下语法:

进口货!宏执行点分名称的编译时导入以查找宏处理器,然后将其注册到当前正在编译的作用域的名称下。

从哪里来的!宏会执行点分名称.name的编译时导入以查找宏处理器,然后将其注册到当前正在编译的作用域的名称下(如果存在,则使用";后面的名称作为";)。

请注意,因为导入了!而且是从那里来的!仅为导入所在的范围定义宏,所有宏的使用必须在显式导入之前!或者是从!以提高清晰度。

Func必须是一个可调用函数,它接受len(Additional_Names)+1个参数,所有这些参数都是抽象语法树,并返回单个抽象语法树。

KIND必须是以下值之一:MARKS。STMT_MACRO宏宏体缩进的语句宏。这是唯一允许使用其他名称的表单。

MARKS.SIBLING_MACRO宏宏宏的主体是下一条语句,它是同一个块。下面的语句将作为其正文移到宏中。

Version用于跟踪宏的版本,以便可以正确缓存生成的字节码。它必须是整数。

ADDIGNMENT_NAMES是宏的其他部分的名称,并且必须是字符串元组。

#(func,_ast.STMT_MACRO,VERSION,())stmt_MACRO!:MULTI_STATEMENT_BODY#(FUNC,_ast.SIBLING_MACRO,VERSION,())SIGBLING_MANK!SINGLE_STATEMENT_BODY#(FUNC,_ast.EXPR_MACRO,VERSION,())x=EXPR_MACRO!(...)#。,))MULTI_PART_MACRO!:MULTI_STATEMENT_BODY后续_宏_PART!

为方便起见,在宏模块中提供了修饰器宏处理器,以将函数标记为宏处理器:

此外,宏处理器将需要一种方式来表示产生值的控制流或副作用代码。为了支持这一点,将添加一个新的ast节点,称为stmt_expr,它结合了语句和表达式。这个新的ast节点将是expr的子类型,但包括一个允许副作用的语句。通过编译语句,然后编译值,它将被编译成字节码。

宏处理器经常需要创建新的变量。这些变量的命名方式需要避免污染原始代码和其他宏。不会强制执行命名规则,但为确保卫生和帮助调试,建议使用以下命名方案:

纯人工变量名应该以$$mname开头,其中mname是宏的名称。

从实变量派生的变量应该以$vname开头,其中vname是变量的名称。

所有变量名都应包含行号和列偏移量,并用下划线分隔。

我看到的真正有价值的宏是在特定的域中,而不是在通用的语言特性中。

运行时编译器(如Numba)必须重新构建Python源代码,或者尝试分析字节码。对于它们来说,直接获取AST会更简单、更可靠:

当匹配表示语法的东西(如Python ast节点或症状表达式)时,与实际语法(而不是表示它的数据结构)进行匹配是很方便的。例如,可以使用域特定的宏来实现计算器以匹配语法:

从…!。AST_matcher导入matchdef culate(Node):if isinstance(node,num):返回node.n匹配!节点:案例!A+b:返回计算(A)+计算(B)大小写!A-b:返回计算(A)-计算(B)大小写!A*b:返回Calculate(A)*Calculate(B)case!A/b:返回Calculate(A)/Calculate(B)。

Def Calculate(Node):if isinstance(node,num):返回节点.n$$Match_4_0=node if isinstance($$Match_4_0,_ast.Add):a,b=$$Match_4_0.Left,$$Match_4_0.right返回Calculate(A)+Calculate(B)Elif isInstance($$Match_4_0,_ast.Sub):a,b=$$Match_4_0.Left,$$Match_4_0.right Return Calculate(A)-Calculate(B)EliisInstance($$Match_4_0,_ast.Mul):a,b=$$Match_4_0.Left,$$Match_4_0.right返回Calculate(A)*Calculate(B)Elif isInstance($$Match_4_0,_ast.Div):a,b=$$Match_4_0.Left,$$Match_4_0.right Return Calculate(A)/Calculate(B)。

注释,无论是装饰符还是PEP3107函数注释,如果它们仅用作检查器的标记或文档,则具有运行时成本。

虽然宏对于特定领域的扩展最有价值,但也可以使用宏来演示可能的语言扩展。

F-string f";...";可以实现为像f!(";...";)一样的宏。

必须注意正确处理返回、中断和继续。上面的代码只是说明性的。

以上将需要特别处理开放。另一种更明确的替代方案是:

有句法宏的语言通常提供用于定义宏的宏。这个PEP故意不这样做,因为目前还不清楚什么是好的设计,我们希望允许社区定义他们自己的宏。

宏定义!名称:输入:...#输入模式,定义元变量输出:...#输出模式,使用元变量。

对于确实使用宏并且已经编译为字节码的代码,检查用于编译代码的宏版本是否与导入的宏处理器匹配将会有一些轻微的开销。

对于没有编译的代码,或者没有用不同版本的宏处理器编译的代码,那么通常会有字节反编译的开销,外加任何额外的宏处理开销。

值得注意的是,源代码到字节码编译的速度在很大程度上与Python性能无关。

为了允许Python代码在编译时转换AST,编译器中的所有AST节点都必须是Python对象。

要有效地做到这一点,将意味着使_ast模块中的所有节点都是不可变的,以便不会对性能造成太大的影响。它们需要是不可变的,以保证AST仍然是一棵树,以避免必须支持循环Foundation。使它们不可变意味着它们将不会有__dict__属性,从而使它们变得紧凑。

目前,所有AST节点都是使用Arena分配器分配的,改用标准分配器可能会稍微减慢编译速度,但在维护方面有优势,因为可以删除很多代码。

本文档放置在公共领域或CC0-1.0-通用许可证下,以许可程度较高者为准。

消息来源:https://github.com/python/peps/blob/master/pep-0638.rst