PEP 638 – 句法宏

猫勺猫勺 03-12 158 阅读 0 评论

抽象

PEPPython 添加了对语法宏的支持。 宏是一个编译时函数,用于转换 程序的一部分,以允许无法实现的功能 用普通的库代码清晰地表达。

术语“句法”意味着这种宏对程序的 语法树。这减少了可能发生的误译机会 使用基于文本的替换宏,并允许实现 的卫生宏。

语法宏允许库在编译过程中修改抽象语法树, 提供为特定域扩展语言的能力,而无需 增加了整个语言的复杂性。

赋予动机

新的语言功能可能会引起争议、破坏性,有时甚至会引起分裂。 Python 现在足够强大和复杂,以至于许多建议添加 由于额外的复杂性,是语言的净损失。

尽管语言的改变可能会使某些模式易于表达, 这将是有代价的。每个新功能都会使语言变得更大, 更难学,更难理解。 Python 曾经被描述为 Python 适合你的大脑, 但随着越来越多的功能被添加,这变得越来越不真实。

由于添加新功能的成本很高, 添加一个只会受益的功能是非常困难或不可能的 一些用户,无论有多少用户,或者该功能有多大的好处 对他们。

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

通过允许语言扩展是模块化和可分发的,就像库一样, 可以实现特定于域的扩展,而不会产生负面影响 该网域以外的用户。 Web 开发人员可能想要一组非常不同的扩展 数据科学家。 我们需要让社区开发自己的扩展。

如果没有某种形式的用户定义的语言扩展, 那些想要保留 语言紧凑,适合他们的大脑,以及那些想要新功能的人 适合他们的领域或编程风格。

提高特定域库的表现力

许多域看到重复的模式是困难的或不可能的 以库的形式表达。 宏可以以更简洁、更不容易出错的方式表达这些模式。

试用新的语言功能

可以使用宏来演示潜在的语言扩展。 例如,宏可以使语句和表达式经过试验。 这样做很可能会带来更高质量的实施 在首次发布时,通过允许更多测试 在这些功能包含在语言中之前。withyield from

几乎不可能确保新功能完全可靠 在发布之前;与 AND 功能相关的错误在发布多年后仍在修复中。withyield from

字节码解释器的长期稳定性

从历史上看,新的语言功能是通过朴素的编译实现的 的 AST 转换为新的复杂字节码指令。 这些字节码通常有自己的内部流控制,执行 可以而且应该在编译器中完成的操作。

例如 直到最近,- 和 语句中的流控制仍由具有上下文相关语义的复杂字节码管理。 这些语句中的控制流现在在编译器中实现,使 口译员更简单、更快捷。tryfinallywith

通过将新功能实现为 AST 转换,现有编译器可以 生成特征的字节码,而无需修改解释器。

如果我们要提高性能和 CPython VM 的可移植性。

理由

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

因为 lisp 是同形的,这意味着 lisp 程序是 lisp 数据结构, LISP 程序可以被 LISP 程序操作。 因此,大部分语言本身都可以定义。

我们想要 Python 中的这种能力, 没有 Lisp 的许多括号。 幸运的是,一种语言不需要同谐性 操纵自己,所需要的只是操纵程序的能力 在解析之后,但在转换为可执行形式之前。

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

规范

语法

词汇分析

后跟感叹号的标识符字符的任何序列 (感叹号,英式英语)将被标记为 .MACRO_NAME

报表形式

macro_stmt = MACRO_NAME testlist [ "import" NAME ] [ "as"  NAME ] [ ":" NEWLINE suite ]

表达形式

macro_expr = MACRO_NAME "(" testlist ")"

解决歧义

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

语义学

汇编

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

对于具有多个名称的宏, 几棵树将被传递给宏处理器, 但只有一个会被退回并替换, 短接封闭的语句块。

这个过程可以重复, 使宏能够返回 AST 节点,包括其他宏。

在到达宏处理器之前,编译器不会查找该宏处理器, 这样内部宏就不需要注册处理器。 例如,在宏中,和 宏不会 需要注册处理器,因为它们将被处理器消除。switchcasedefaultswitch

要启用要导入的宏的定义, 宏和是预定义的。 它们支持以下语法:import!from!

"import!" dotted_name "as" name"from!" dotted_name "import" name [ "as" name ]

宏执行编译时导入以查找宏处理器,然后将其注册到当前正在编译的作用域。import!dotted_namename

宏执行编译时导入以查找宏处理器,然后在下注册它(使用以下“as”,如果存在) 对于当前正在编译的范围。from!dotted_name.namenamename

请注意,由于 和 仅定义 在存在导入的范围内,宏的所有用法都必须以 明确或提高清晰度。import!from!import!from!

例如,要从“my.compiler”导入宏“compile”:

from! my.compiler import compile

定义宏处理器

  • 宏处理器由四元组定义,包括:(func, kind, version, additional_names)

  • func必须是接受参数(所有参数都是抽象语法树)并返回单个抽象语法树的可调用对象。len(additional_names)+1

  • kind必须是以下项之一:

  • macros.STMT_MACRO:缩进宏主体的语句宏。这是唯一允许有其他名称的表单。

  • macros.SIBLING_MACRO:语句宏,其中宏的主体是同一块中的下一个语句。以下语句作为其正文移动到宏中。

  • macros.EXPR_MACRO:表达式宏。

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

  • additional_names是宏的附加部分的名称,并且必须是字符串的元组。

# (func, _ast.STMT_MACRO, VERSION, ())
stmt_macro!:
   multi_statement_body

# (func, _ast.SIBLING_MACRO, VERSION, ())
sibling_macro!
single_statement_body

# (func, _ast.EXPR_MACRO, VERSION, ())
x = expr_macro!(...)

# (func, _ast.STMT_MACRO, VERSION, ("subsequent_macro_part",))
multi_part_macro!:
   multi_statement_body
subsequent_macro_part!:
   multi_statement_body

编译器将检查使用的语法是否与声明的种类匹配。

为方便起见,模块中提供了装饰器,用于将函数标记为宏处理器:macro_processormacros

def macro_processor(kind, version, *additional_names):
   def deco(func):
       return func, kind, version, additional_names
   return deco

这可用于帮助声明宏处理器,例如:

@macros.macro_processor(macros.STMT_MACRO, 1_08)def switch(astnode):

AST 扩展

需要两个新的 AST 节点来表示宏,并且 .macro_stmtmacro_expr

class macro_stmt(_ast.stmt):
    _fields = "name", "args", "importname", "asname", "body"class macro_expr(_ast.expr):
    _fields = "name", "args"

此外,宏处理器将需要一种方法来表达控制流或产生值的副作用代码。 将添加一个名为的新 AST 节点,该节点将语句和表达式组合在一起。 这个新的 ast 节点将是 的子类型,但包含一个允许副作用的语句。 它将通过编译语句,然后编译值来编译为字节码。stmt_exprexpr

class stmt_expr(_ast.expr):
    _fields = "stmt", "value"

卫生和调试

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

  • 所有生成的变量名称都应以$

  • 纯人工变量名称应从宏的名称开始。$$mnamemname

  • 从实变量派生的变量应从变量名称开始。$vnamevname

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

例子:

  • 纯生成的名称:$$macro_17_0

  • 从表达式宏的变量派生的名称:$var_12_5

例子

编译时检查的数据结构

在 Python 中将数据表编码为大型字典是很常见的。 但是,这些可能难以维护且容易出错。 宏允许以更具可读性的格式写入此类数据。 然后,在编译时,可以验证数据并将其转换为有效的格式。

例如,假设我们有两个字典文字将代码映射到名称, 反之亦然。 这很容易出错,因为字典可能有重复的键, 或者一个表可能不是另一个表的倒数。 宏可以从单个表生成两个映射,并且 同时,验证是否存在重复项。

color_to_code = {
    "red": 1,
    "blue": 2,
    "green": 3,}code_to_color = {
    1: "red",
    2: "blue",
    3: "yellow", # error}

将变为:

bijection! color_to_code, code_to_color:
    "red" = 1
    "blue" = 2
    "green" = 3

特定于域的扩展

我认为宏具有真正价值的地方是在特定领域,而不是在通用语言功能中。

例如,解析器。 下面是使用 macros 的 Python 解析器定义的一部分:

choice! single_input:
    NEWLINE
    simple_stmt
    sequence!:
        compound_stmt
        NEWLINE

编译 器

运行时编译器,例如必须重构 Python 源代码,或尝试分析字节码。 对他们来说,直接获取 AST 会更简单、更可靠:numba

from! my.jit.library import jit

jit!
def func():

匹配符号表达式

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

from! ast_matcher import match

def calculate(node):
    if isinstance(node, Num):
        return node.n
    match! node:
        case! a + b:
            return calculate(a) + calculate(b)
        case! a - b:
            return calculate(a) - calculate(b)
        case! a * b:
            return calculate(a) * calculate(b)
        case! a / b:
            return calculate(a) / calculate(b)

可以转换为:

def calculate(node):
    if isinstance(node, Num):
        return node.n
    $$match_4_0 = node
    if isinstance($$match_4_0, _ast.Add):
        a, b = $$match_4_0.left, $$match_4_0.right
        return 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)
    elif isinstance($$match_4_0, _ast.Mul):
        a, b = $$match_4_0.left, $$match_4_0.right
        return 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)

零成本标记和注释

注释(装饰器或 PEP 3107 函数注释)具有运行时成本 即使它们仅用作跳棋的标记或文档。

@do_nothing_markerdef foo(...):
    ...

可以替换为零成本宏:

do_nothing_marker!:
def foo(...):
    ...

原型语言扩展

尽管宏对于特定于域的扩展最有价值,但可以 使用宏演示可能的语言扩展。

F 字符串:

f 字符串可以像 一样实现为 宏。 读起来不太好,但对于实验仍然有用。f"..."f!("...")

Try finally 语句:

try_!:
    body
finally!:
    closing

大致翻译为:

try:
    bodyexcept:
    closingelse:
    closing

注意:

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

附语句:

with! open(filename) as fd:
    return fd.read()

以上情况需要特别处理。 更明确的替代方案是:open

with! open!(filename) as fd:
    return fd.read()

宏定义宏

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

一种可能的形式是:

macro_def! name:
    input:
        ... # input pattern, defining meta-variables
    output:
        ... # output pattern, using meta-variables

向后兼容

此 PEP 完全向后兼容。

 

性能影响

对于不使用宏的代码,不会对性能产生影响。

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

对于尚未编译或使用不同版本编译的代码 那么就会有通常的字节码开销 编译,加上宏处理的任何额外开销。

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

实现

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

要有效地做到这一点,就意味着要使模块中的所有节点都存在 不可变,以免大幅降低性能。 它们需要是不可变的,以保证 AST 仍然是一棵树,以避免必须支持循环 GC。 使它们不可变意味着它们将没有属性,从而使它们紧凑。_ast__dict__

模块中的 AST 节点将保持可变状态。ast

目前,所有 AST 节点都使用竞技场分配器进行分配。 更改为使用标准分配器可能会稍微减慢编译速度, 但在维护方面具有优势,因为可以删除许多代码。

参考实现

目前还没有。

版权

本文档被置于公共领域或位于 CC0-1.0-通用许可证,以更宽松的许可证为准。

The End 微信扫一扫

文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。

本文作者:猫勺本文链接:https://www.jo6.cn/post/52.html

上一篇 下一篇

相关阅读

发表评论

访客 访客
快捷回复: 表情:
评论列表 (暂无评论,158人围观)

还没有评论,来说两句吧...

取消
微信二维码
微信二维码
支付宝二维码