Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 638 – 句法宏

作者:
Mark Shannon <mark at hotpy.org>
讨论至:
Python-Dev 帖子
状态:
草案
类型:
标准跟踪
创建日期:
2020年9月24日
发布历史:
2020年9月26日

目录

摘要

本PEP为Python添加了对句法宏的支持。宏是一个编译时函数,它转换程序的一部分,以实现无法在普通库代码中清晰表达的功能。

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

句法宏允许库在编译期间修改抽象语法树,从而能够为特定领域扩展语言,而无需增加整个语言的复杂性。

动机

新的语言特性可能会引起争议、破坏性,有时甚至导致分裂。Python现在功能强大且复杂,以至于许多提议的添加由于额外的复杂性而对语言造成了净损失。

尽管语言更改可能使某些模式易于表达,但它会付出代价。每个新特性都会使语言变得更大、更难学习和理解。Python曾被描述为Python符合你的思维,但随着越来越多的特性被添加,这种说法变得越来越不真实。

由于添加新功能的成本很高,因此,无论用户数量多少,无论该功能对他们有多大的好处,都很难或不可能添加仅对某些用户有益的功能。

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

通过允许语言扩展像库一样模块化和可分发,可以在不影响该领域之外的用户的情况下实现领域特定扩展。一个Web开发者可能需要与数据科学家非常不同的扩展集。我们需要让社区开发自己的扩展。

如果没有某种形式的用户定义语言扩展,那些希望保持语言紧凑并符合其思维的人与那些希望获得适合其领域或编程风格的新功能的人之间将持续不断地斗争。

提高特定领域库的表达能力

许多领域都存在难以或不可能作为库表达的重复模式。宏可以以更简洁、更不易出错的方式表达这些模式。

试用新的语言特性

可以使用宏来演示潜在的语言扩展。例如,宏将使with语句和yield from表达式得以试用。这样做很可能在首次发布时带来更高质量的实现,因为这些功能在被纳入语言之前可以进行更多的测试。

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

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

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

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

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

如果我们要提高CPython VM的性能和可移植性,稳定的解释器是必要的。

基本原理

Python既富有表现力又易于学习;它被广泛认为是易学、广泛使用的编程语言。然而,它并不是最灵活的。这个称号属于Lisp。

因为Lisp是同像的,这意味着Lisp程序是Lisp数据结构,所以Lisp程序可以被Lisp程序操纵。因此,大部分语言都可以用自身来定义。

我们希望Python也具备这种能力,而无需Lisp特有的许多括号。幸运的是,语言能够自我操作并不需要同像性,所需要的只是在解析之后、转换为可执行形式之前操纵程序的能力。

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

规范

语法

词法分析

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

语句形式

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

表达式形式

macro_expr = MACRO_NAME "(" testlist ")"

解决歧义

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

语义

编译

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

对于具有多个名称的宏,将向宏处理器传递多个树,但只有一个会被返回和替换,从而缩短包含语句块。

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

编译器不会查找宏处理器,直到达到该宏,因此内部宏不需要注册处理器。例如,在switch宏中,casedefault宏不需要注册处理器,因为它们将被switch处理器消除。

为了实现宏定义的导入,预定义了宏import!from!。它们支持以下语法

"import!" dotted_name "as" name

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

import!宏执行dotted_name的编译时导入以查找宏处理器,然后将其注册在当前正在编译的范围的name下。

from!宏执行dotted_name.name的编译时导入以查找宏处理器,然后将其注册在当前正在编译的范围的name下(如果存在“as”后面的name,则使用该名称)。

请注意,由于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

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

为了方便,macros模块中提供了装饰器macro_processor,用于将函数标记为宏处理器

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"

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

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

卫生和调试

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

  • 所有生成的变量名都应以$开头
  • 纯人工变量名应以$$mname开头,其中mname是宏的名称。
  • 派生自真实变量的变量应以$vname开头,其中vname是变量的名称。
  • 所有变量名都应包含行号和列偏移量,用下划线分隔。

示例

  • 纯生成名称:$$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

领域特定扩展

我认为宏的真正价值在于特定领域,而非通用语言特性。

例如,解析器。这是Python解析器定义的一部分,使用宏

choice! single_input:
    NEWLINE
    simple_stmt
    sequence!:
        compound_stmt
        NEWLINE

编译器

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

from! my.jit.library import jit

jit!
def func():
    ...

匹配符号表达式

当匹配代表语法的事物时,例如Python ast节点或sympy表达式,直接匹配实际语法而不是表示它的数据结构会很方便。例如,可以使用领域特定宏来实现一个计算器来匹配语法

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_marker
def foo(...):
    ...

可以用零成本宏替换

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

原型语言扩展

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

f-字符串

f-字符串f"..."可以作为宏f!("...")实现。虽然读起来没那么好,但仍可用于试验。

Try finally 语句
try_!:
    body
finally!:
    closing

大致翻译为

try:
    body
except:
    closing
else:
    closing
注意
必须注意正确处理返回、中断和继续。以上代码仅为说明。
With 语句
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模块中的所有节点都不可变,以便不会严重降低性能。它们需要不可变以保证AST仍然是**树**,以避免不得不支持循环GC。使它们不可变意味着它们不会有__dict__属性,从而使其紧凑。

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

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

参考实现

暂无。


来源:https://github.com/python/peps/blob/main/peps/pep-0638.rst

上次修改:2025-02-01 08:55:40 GMT