ast --- 抽象语法树

源代码: Lib/ast.py


ast 模块帮助 Python 程序处理 Python 语法的抽象语法树。抽象语法或许会随着 Python 的更新发布而改变;该模块能够帮助理解当前语法在编程层面的样貌。

抽象语法树可通过将 ast.PyCF_ONLY_AST 作为旗标传递给 compile() 内置函数来生成,或是使用此模块中提供的 parse() 辅助函数。返回结果将是一个对象树,,其中的类都继承自 ast.AST。抽象语法树可被内置的 compile() 函数编译为一个 Python 代码对象。

节点类

class ast.AST

这是所有 AST 节点类的基类。实际上,这些节点类派生自 Parser/Python.asdl 文件,其中定义的语法树示例 如下。它们在 C 语言模块 _ast 中定义,并被导出至 ast 模块。

抽象语法定义的每个左侧符号(比方说, ast.stmt 或者 ast.expr)定义了一个类。另外,在抽象语法定义的右侧,对每一个构造器也定义了一个类;这些类继承自树左侧的类。比如,ast.BinOp 继承自 ast.expr。对于多分支产生式(也就是"和规则"),树右侧的类是抽象的;只有特定构造器结点的实例能被构造。

_fields

每个具体类都有个属性 _fields, 用来给出所有子节点的名字。

每个具体类的实例对它每个子节点都有一个属性,对应类型如文法中所定义。比如,ast.BinOp 的实例有个属性 left,类型是 ast.expr.

如果这些属性在文法中标记为可选(使用问号),对应值可能会是 None。如果这些属性有零或多个(用星号标记),对应值会用Python的列表来表示。所有可能的属性必须在用 compile() 编译得到AST时给出,且是有效的值。

lineno
col_offset
end_lineno
end_col_offset

Instances of ast.expr and ast.stmt subclasses have lineno, col_offset, lineno, and col_offset attributes. The lineno and end_lineno are the first and last line numbers of source text span (1-indexed so the first line is line 1) and the col_offset and end_col_offset are the corresponding UTF-8 byte offsets of the first and last tokens that generated the node. The UTF-8 offset is recorded because the parser uses UTF-8 internally.

注意编译器不需要结束位置,所以结束位置是可选的。结束偏移在最后一个符号*之后*,例如你可以通过 source_line[node.col_offset : node.end_col_offset] 获得一个单行表达式节点的源码片段。

一个类的构造器 ast.T 像下面这样parse它的参数。

  • 如果有位置参数,它们必须和 T._fields 中的元素一样多;他们会像这些名字的属性一样被赋值。

  • 如果有关键字参数,它们必须被设为和给定值同名的属性。

比方说,要创建和填充节点 ast.UnaryOp,你得用

node = ast.UnaryOp()
node.op = ast.USub()
node.operand = ast.Constant()
node.operand.value = 5
node.operand.lineno = 0
node.operand.col_offset = 0
node.lineno = 0
node.col_offset = 0

或者更紧凑点

node = ast.UnaryOp(ast.USub(), ast.Constant(5, lineno=0, col_offset=0),
                   lineno=0, col_offset=0)

在 3.8 版更改: ast.Constant 现在用于所有常量。

3.8 版后已移除: Old classes ast.Num, ast.Str, ast.Bytes, ast.NameConstant and ast.Ellipsis are still available, but they will be removed in future Python releases. In the meanwhile, instantiating them will return an instance of a different class.

抽象文法

抽象文法目前定义如下

-- ASDL's 5 builtin types are:
-- identifier, int, string, object, constant

module Python
{
    mod = Module(stmt* body, type_ignore *type_ignores)
        | Interactive(stmt* body)
        | Expression(expr body)
        | FunctionType(expr* argtypes, expr returns)

        -- not really an actual node but useful in Jython's typesystem.
        | Suite(stmt* body)

    stmt = FunctionDef(identifier name, arguments args,
                       stmt* body, expr* decorator_list, expr? returns,
                       string? type_comment)
          | AsyncFunctionDef(identifier name, arguments args,
                             stmt* body, expr* decorator_list, expr? returns,
                             string? type_comment)

          | ClassDef(identifier name,
             expr* bases,
             keyword* keywords,
             stmt* body,
             expr* decorator_list)
          | Return(expr? value)

          | Delete(expr* targets)
          | Assign(expr* targets, expr value, string? type_comment)
          | AugAssign(expr target, operator op, expr value)
          -- 'simple' indicates that we annotate simple name without parens
          | AnnAssign(expr target, expr annotation, expr? value, int simple)

          -- use 'orelse' because else is a keyword in target languages
          | For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment)
          | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment)
          | While(expr test, stmt* body, stmt* orelse)
          | If(expr test, stmt* body, stmt* orelse)
          | With(withitem* items, stmt* body, string? type_comment)
          | AsyncWith(withitem* items, stmt* body, string? type_comment)

          | Raise(expr? exc, expr? cause)
          | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody)
          | Assert(expr test, expr? msg)

          | Import(alias* names)
          | ImportFrom(identifier? module, alias* names, int? level)

          | Global(identifier* names)
          | Nonlocal(identifier* names)
          | Expr(expr value)
          | Pass | Break | Continue

          -- XXX Jython will be different
          -- col_offset is the byte offset in the utf8 string the parser uses
          attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset)

          -- BoolOp() can use left & right?
    expr = BoolOp(boolop op, expr* values)
         | NamedExpr(expr target, expr value)
         | BinOp(expr left, operator op, expr right)
         | UnaryOp(unaryop op, expr operand)
         | Lambda(arguments args, expr body)
         | IfExp(expr test, expr body, expr orelse)
         | Dict(expr* keys, expr* values)
         | Set(expr* elts)
         | ListComp(expr elt, comprehension* generators)
         | SetComp(expr elt, comprehension* generators)
         | DictComp(expr key, expr value, comprehension* generators)
         | GeneratorExp(expr elt, comprehension* generators)
         -- the grammar constrains where yield expressions can occur
         | Await(expr value)
         | Yield(expr? value)
         | YieldFrom(expr value)
         -- need sequences for compare to distinguish between
         -- x < 4 < 3 and (x < 4) < 3
         | Compare(expr left, cmpop* ops, expr* comparators)
         | Call(expr func, expr* args, keyword* keywords)
         | FormattedValue(expr value, int? conversion, expr? format_spec)
         | JoinedStr(expr* values)
         | Constant(constant value, string? kind)

         -- the following expression can appear in assignment context
         | Attribute(expr value, identifier attr, expr_context ctx)
         | Subscript(expr value, slice slice, expr_context ctx)
         | Starred(expr value, expr_context ctx)
         | Name(identifier id, expr_context ctx)
         | List(expr* elts, expr_context ctx)
         | Tuple(expr* elts, expr_context ctx)

          -- col_offset is the byte offset in the utf8 string the parser uses
          attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset)

    expr_context = Load | Store | Del | AugLoad | AugStore | Param

    slice = Slice(expr? lower, expr? upper, expr? step)
          | ExtSlice(slice* dims)
          | Index(expr value)

    boolop = And | Or

    operator = Add | Sub | Mult | MatMult | Div | Mod | Pow | LShift
                 | RShift | BitOr | BitXor | BitAnd | FloorDiv

    unaryop = Invert | Not | UAdd | USub

    cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn

    comprehension = (expr target, expr iter, expr* ifs, int is_async)

    excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body)
                    attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset)

    arguments = (arg* posonlyargs, arg* args, arg? vararg, arg* kwonlyargs,
                 expr* kw_defaults, arg? kwarg, expr* defaults)

    arg = (identifier arg, expr? annotation, string? type_comment)
           attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset)

    -- keyword arguments supplied to call (NULL identifier for **kwargs)
    keyword = (identifier? arg, expr value)

    -- import name with optional 'as' alias.
    alias = (identifier name, identifier? asname)

    withitem = (expr context_expr, expr? optional_vars)

    type_ignore = TypeIgnore(int lineno, string tag)
}

ast 中的辅助函数

除了节点类, ast 模块里为遍历抽象语法树定义了这些工具函数和类:

ast.parse(source, filename='<unknown>', mode='exec', *, type_comments=False, feature_version=None)

把源码解析为AST节点。和 compile(source, filename, mode,ast.PyCF_ONLY_AST) 等价。

如果给出 type_comments=True,解析器会被修改以检查并返回 PEP 484PEP 526 所描述的类型注释。 这相当于将 ast.PyCF_TYPE_COMMENTS 添加到传给 compile() 的旗标中。 这将报告针对未在正确放置类型注释的语法错误。 没有这个旗标,类型注释将被忽略,而指定 AST 节点上的 type_comment 字段将总是为 None。 此外,# type: ignore 注释的位置将作为 Moduletype_ignores 属性被返回(在其他情况下则总是为空列表)。

并且,如果 mode'func_type',则输入语法会进行与 PEP 484 "签名类型注释" 对应的修改,例如 (str, int) -> List[str]

此外,将 feature_version 设为元组 (major, minor) 将会尝试使用该will attempt to parse using that Python 版本的语法来进行解析。 目前 major 必须等于 3。 例如,设置 feature_version=(3, 4) 将允许使用 asyncawait 作为变量名。 最低受支持版本为 (3, 4);最高则为 sys.version_info[0:2]

警告

足够复杂或是巨大的字符串可能导致Python解释器的崩溃,因为Python的AST编译器是有栈深限制的。

在 3.8 版更改: 增加了 type_comments, mode='func_type'feature_version

ast.literal_eval(node_or_string)

对表达式节点以及包含Python字面量或容器的字符串进行安全的求值。传入的字符串或者节点里可能只包含下列的Python字面量结构: 字符串,字节对象(bytes),数值,元组,列表,字典,集合,布尔值和 None

这可被用于安全地对包含不受信任来源的 Python 值的字符串进行求值而不必解析这些值本身。 它并不能对任意的复杂表达式进行求值,例如涉及运算符或索引操作的表达式。

警告

足够复杂或是巨大的字符串可能导致Python解释器的崩溃,因为Python的AST编译器是有栈深限制的。

在 3.2 版更改: 目前支持字节和集合。

ast.get_docstring(node, clean=True)

返回给定 node (必须为 FunctionDef, AsyncFunctionDef, ClassDefModule 节点) 的文档字符串,或者如果没有文档字符串则返回 None。 如果 clean 为真值,则通过 inspect.cleandoc() 清除文档字符串的缩进。

在 3.5 版更改: 目前支持 AsyncFunctionDef

ast.get_source_segment(source, node, *, padded=False)

获取生成 nodesource 的源代码段。 如果丢失了某些位置信息 (lineno, end_lineno, col_offsetend_col_offset),则返回 None

如果 paddedTrue,则多行语句的第一行将以与其初始位置相匹配的空格填充。

3.8 新版功能.

ast.fix_missing_locations(node)

当你通过 compile() 来编译节点树时,编译器会准备接受每个支持 linenocol_offset 属性的节点的相应信息。 对已生成节点来说这是相当繁琐的,因此这个辅助工具会递归地为尚未设置这些属性的节点添加它们,具体做法是将其设为父节点的对应值。 它将从 node 开始递归地执行。

ast.increment_lineno(node, n=1)

node 开始按 n 递增节点树中每个节点的行号和结束行号。 这在“移动代码”到文件中的不同位置时很有用处。

ast.copy_location(new_node, old_node)

在可能的情况下将源位置 (lineno, col_offset, end_linenoend_col_offset) 从 old_node 拷贝到 new_node,并返回 new_node

ast.iter_fields(node)

针对于 node 上在 node._fields 中出现的每个字段产生一个 (fieldname, value) 元组。

ast.iter_child_nodes(node)

产生 node 所有的直接子节点,也就是说,所有为节点的字段所有为节点列表的字段条目。

ast.walk(node)

递归地产生节点树中从 node 开始(包括 node 本身)的所有下级节点,没有确定的排序方式。 这在你仅想要原地修改节点而不关心具体上下文时很有用处。

class ast.NodeVisitor

一个遍历抽象语法树并针对所找到的每个节点调用访问器函数的节点访问器基类。 该函数可能会返回一个由 visit() 方法所提供的值。

这个类应当被子类化,并由子类来添加访问器方法。

visit(node)

访问一个节点。 默认实现会调用名为 self.visit_classname 的方法其中 classname 为节点类的名称,或者如果该方法不存在则为 generic_visit()

generic_visit(node)

该访问器会在节点的所有子节点上调用 visit()

请注意所有包含自定义访问器方法的节点的子节点将不会被访问除非访问器调用了 generic_visit() 或是自行访问它们。

如果你想在遍历期间应用对节点的修改则请不要使用 NodeVisitor。 对此目的可使用一个允许修改的特殊访问器 (NodeTransformer)。

3.8 版后已移除: visit_Num(), visit_Str(), visit_Bytes(), visit_NameConstant()visit_Ellipsis() 等方法现在已被弃用且在未来的 Python 版本中将不会再被调用。 请添加 visit_Constant() 方法来处理所有常量节点。

class ast.NodeTransformer

子类 NodeVisitor 用于遍历抽象语法树,并允许修改节点。

NodeTransformer 将遍历抽象语法树并使用visitor方法的返回值去替换或移除旧节点。如果visitor方法的返回值为 None , 则该节点将从其位置移除,否则将替换为返回值。当返回值是原始节点时,无需替换。

如下是一个转换器示例,它将所有出现的名称 (foo) 重写为 data['foo']:

class RewriteName(NodeTransformer):

    def visit_Name(self, node):
        return Subscript(
            value=Name(id='data', ctx=Load()),
            slice=Index(value=Constant(value=node.id)),
            ctx=node.ctx
        )

请记住,如果您正在操作的节点具有子节点,则必须先转换其子节点或为该节点调用 generic_visit() 方法。

对于属于语句集合(适用于所有语句节点)的节点,访问者还可以返回节点列表而不仅仅是单个节点。

如果 NodeTransformer 引入了新的(不属于原节点树一部分的)节点而没有给出它们的位置信息(如 lineno 等),则应当调用 fix_missing_locations() 并传入新的子节点树来重新计算位置信息:

tree = ast.parse('foo', mode='eval')
new_tree = fix_missing_locations(RewriteName().visit(tree))

通常你可以像这样使用转换器:

node = YourTransformer().visit(node)
ast.dump(node, annotate_fields=True, include_attributes=False)

返回 node 中树结构的格式化转储。 这主要适用于调试目的。 如果 annotate_fields 为真值(默认),返回的字符串将显示字段的名称和值。 如果 annotate_fields 为假值,结果字符串将通过省略无歧义的字段名称变得更为紧凑。 默认情况下不会转储行号和列偏移等属性。 如果需要,可将 include_attributes 设为真值。

参见

Green Tree Snakes,一个外部文档资源,包含处理 Python AST 的完整细节。

ASTTokens 会为 Python AST 标注生成它们的源代码中的形符和文本的位置。 这对执行源代码转换的工具很有帮助。

leoAst.py 通过在形符和 ast 节点之间插入双向链接统一了 Python 程序基于形符的和基于解析树的视图。

LibCST 将代码解析为一个实体语法树(Concrete Syntax Tree),它看起来像是 ast 树而又保留了所有格式化细节。 它对构建自动化重构(codemod)应用和代码质量检查工具很有用处。

Parso 是一个支持错误恢复和不同 Python 版本的(在多个 Python 版本中)往返解析的 Python 解析器。 Parso 还能列出你的 Python 文件中的许多错误。