PEP 638 – 语法宏
- 作者:
- Mark Shannon <mark at hotpy.org>
- 讨论邮件列表:
- Python-Dev 线程
- 状态:
- 草稿
- 类型:
- 标准轨迹
- 创建:
- 2020-09-24
- 修订历史:
- 2020-09-26
摘要
此 PEP 为 Python 添加了对语法宏的支持。宏是在编译时转换程序一部分的函数,以允许无法在普通库代码中清晰表达的功能。
术语“语法”表示此类宏在程序的语法树上操作。这减少了基于文本的替换宏可能发生的误译的可能性,并允许实现卫生宏。
语法宏允许库在编译期间修改抽象语法树,从而能够扩展特定领域的语言,而无需增加整个语言的复杂性。
动机
新的语言特性可能存在争议,具有破坏性,有时还会引起分歧。Python 现在已经足够强大和复杂,因此许多提出的新增功能由于增加了复杂性而对语言造成了净损失。
虽然语言更改可能使某些模式易于表达,但它也会付出代价。每个新特性都会使语言变得更大、更难学习和更难理解。Python 曾经被描述为Python 适合你的大脑,但随着越来越多特性的添加,这一点越来越不成立。
由于添加新特性的成本很高,因此很难或不可能添加一项仅对某些用户有益的特性,无论有多少用户,或者该特性对他们有多大益处。
Python 在数据科学和机器学习中的使用在过去几年中发展非常迅速。但是,Python 的大多数核心开发者都没有数据科学或机器学习背景。这使得核心开发者极难确定机器学习的语言扩展是否值得。
通过允许语言扩展像库一样模块化和可分发,特定领域的扩展可以在不影响该领域之外的用户的情况下实现。Web 开发人员可能希望获得与数据科学家非常不同的扩展集。我们需要让社区开发自己的扩展。
如果没有某种形式的用户定义语言扩展,那么那些希望保持语言简洁并适合其大脑的人与那些希望获得适合其领域或编程风格的新特性的人之间将持续存在冲突。
提高特定领域库的表现力
许多领域看到了难以或不可能用库表达的重复模式。宏可以以更简洁、更不易出错的方式表达这些模式。
试用新的语言特性
可以使用宏来演示潜在的语言扩展。例如,宏本可以使with
语句和yield from
表达式得到试用。这样做很可能会导致在首次发布时获得更高质量的实现,因为在这些特性包含在语言中之前可以进行更多测试。
几乎不可能确保在发布新特性之前它是完全可靠的;与with
和yield from
特性相关的错误在它们发布多年后仍在修复。
字节码解释器的长期稳定性
历史上,新的语言特性是通过将 AST 转换为新的、复杂的字节码指令来实现的。这些字节码通常具有自己的内部流程控制,执行本可以在编译器中完成的操作。
例如,直到最近,try
-finally
和with
语句中的流程控制都是由具有上下文相关语义的复杂字节码管理的。这些语句中的控制流现在在编译器中实现,使解释器更简单、更快。
通过将新特性实现为 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)
将被解析为宏语句,而不是包含宏表达式的表达式语句。
语义
编译
在将代码转换为字节码时遇到macro
时,代码生成器将查找为该宏注册的宏处理器,并将以宏为根的 AST 传递给处理器函数。然后,返回的 AST 将替换原始树。
对于具有多个名称的宏,将向宏处理器传递多个树,但只会返回并替换一个树,从而缩短包含语句块。
此过程可以重复,以使宏能够返回包括其他宏的 AST 节点。
编译器在到达该宏之前不会查找宏处理器,因此内部宏不需要注册处理器。例如,在switch
宏中,case
和default
宏不需要注册处理器,因为它们将被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
编译器将检查所使用的语法是否与声明的类型匹配。
为方便起见,装饰器macro_processor
在macros
模块中提供,以将函数标记为宏处理器
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_stmt
和 macro_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 节点都是使用 arena 分配器分配的。更改为使用标准分配器可能会稍微减慢编译速度,但在维护方面具有优势,因为可以删除大量代码。
参考实现
目前还没有。
版权
本文档放置在公共领域或根据 CC0-1.0-Universal 许可证,以较宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0638.rst
上次修改时间:2023-09-09 17:39:29 GMT