19. 解析
19. 解析
解析是将令牌流转换为语法结构的阶段。
分词器识别词汇单元。解析器识别语法结构。它决定一个标记序列是否是一个有效的Python程序,并且当它有效时,构建一个抽象语法树。
对于这个来源:python def add(a, b): return a + b 标记器生成标记,例如:text NAME "def" NAME "add" LPAR "(" NAME "a" COMMA "," NAME "b" RPAR ")" COLON ":" NEWLINE "\n" INDENT " " NAME "return" NAME "a" PLUS "+" NAME "b" NEWLINE "\n" DEDENT "" ENDMARKER "" 解析器给出这些标记结构:text Module FunctionDef name="add" arguments arg "a" arg "b" body Return BinOp Name "a" Add Name "b" 差异很重要。标记化知道def, add, (, a, 和:存在。解析知道它们一起形成一个函数定义。
19.1 在编译管道中的位置
解析位于标记化和语义编译之间。```text source bytes ↓ encoding detection ↓ tokenization ↓ parsing ↓ abstract syntax tree ↓ symbol table ↓ compiler ↓ code object ↓ bytecode execution
其主要职责是:```text
recognize valid Python grammar
reject invalid syntax
group tokens into statements and expressions
apply operator precedence
handle indentation-based blocks
build AST nodes
produce useful syntax errors
```Python 语言参考公开了 CPython 使用的完整语法,源自`Grammar/python.gram`,同时省略与代码生成和错误恢复相关的实现细节。 ([Python 文档][1])
## 19.2 CPython 使用 PEG 解析器
现代 CPython 使用 PEG 解析器。
PEG 表示解析表达式语法。 CPython 在 Python 3.9 中从其较旧的 LL(1) 解析器切换为 PEG 解析器。 PEG解析器允许更直接地表达Python的语法,并在添加语法时为CPython提供更大的灵活性。
PEG 解析器的工作原理是按顺序尝试语法替代方案。选择第一个成功的替代方案。
简化的规则可能如下所示:```text
statement:
| function_def
| class_def
| if_stmt
| while_stmt
| simple_stmt
```给定输入标记,解析器尝试匹配语句。如果第一个替代方案失败,它将尝试下一个,依此类推。
这种有序选择不同于某些上下文无关语法系统,其中替代项是无序的。在 PEG 中,顺序很重要。
## 19.3 语法规则
该语法定义了有效的标记序列。
一个简化的函数定义规则可以想象如下:```text
function_def:
"def" NAME "(" parameters? ")" ":" block
```这意味着:```text
the keyword def
then a function name
then an opening parenthesis
then optional parameters
then a closing parenthesis
then a colon
then a block
```真正的 CPython 语法更加详细,因为它支持装饰器、异步函数、注释、仅位置参数、仅关键字参数、默认值、类型参数、返回注释和错误恢复。
尽管如此,基本模型仍然是:```text
tokens are matched against grammar rules
rules call other rules
successful rules produce AST structure
failed rules produce backtracking or syntax errors
```## 19.4 语句和表达式
Python 语法有语句和表达式。
语句执行操作:```python
x = 1
return x
if x:
print(x)
```表达式产生值:```python
x + 1
f(a, b)
items[0]
lambda x: x + 1
```解析器必须区分它们,因为并非每个表达式都可以在任何地方使用,也不是每个语句都可以出现在表达式内。
有效的:```python
if ready:
run()
```无效的:```python
x = if ready: run()
```有效的:```python
value = a if ready else b
```语法对这些区别进行了编码。`if`作为一个语句属于一个语法区域。条件表达式属于另一个表达式。
## 19.5 街区和套房
Python 块表示为`INDENT`和`DEDENT`代币。
解析器不直接计算空格。分词器已经将缩进转换为结构标记。
例子:```python
if x:
a = 1
b = 2
c = 3
```代币形状:```text
NAME "if"
NAME "x"
COLON ":"
NEWLINE
INDENT
NAME "a"
EQUAL
NUMBER "1"
NEWLINE
NAME "b"
EQUAL
NUMBER "2"
NEWLINE
DEDENT
NAME "c"
EQUAL
NUMBER "3"
NEWLINE
ENDMARKER
```解析器将块视为一个套件:```text
If
test: Name "x"
body:
Assign a = 1
Assign b = 2
Module body:
Assign c = 3
```这种设计使缩进处理不受大多数语法规则的影响。语法可以说“块”并依赖于分词器生成`INDENT`和`DEDENT`代币。
## 19.6 表达式解析和优先级
解析表达式需要优先级。
考虑:```python
x = 1 + 2 * 3
```解析器必须产生:```text
1 + (2 * 3)
```不是:```text
(1 + 2) * 3
```AST 形状为:```text
Assign
target: Name "x"
value:
BinOp Add
left: Constant 1
right:
BinOp Mult
left: Constant 2
right: Constant 3
```语法规则通过分层表达式对优先级进行编码。
简化模型:```text
expression
conditional expression
sum
term
sum "+" term
sum "-" term
term
factor
term "*" factor
term "/" factor
factor
atom
"-" factor
"+" factor
atom
NAME
NUMBER
STRING
"(" expression ")"
```较高优先级的结构在语法中显得更深。乘法比加法绑定更紧密,因为加法规则使用乘法级表达式作为操作数。
## 19.7 关联性
优先级决定哪些运算符绑定更紧密。结合性决定了相同优先级组的运算符如何结合。
例子:```python
x = a - b - c
```这解析为:```text
(a - b) - c
```不是:```text
a - (b - c)
```减法是左结合的。
但求幂的行为有所不同:```python
x = a ** b ** c
```这解析为:```text
a ** (b ** c)
```语法必须对此进行显式编码。关联性不是运行时属性。它在评估开始之前由解析结构修复。
## 19.8 调用、属性和下标
Python 有后缀表达式形式:```python
obj.attr
obj.method(x)
items[i]
items[i:j]
f(x)(y).z
```这些链条紧紧相连。
例子:```python
result = client.session.get(url).json()["items"][0]
```解析器将其分组为对主表达式的一系列操作:```text
Name "client"
Attribute "session"
Attribute "get"
Call args=[Name "url"]
Attribute "json"
Call args=[]
Subscript "items"
Subscript 0
```这就是为什么调用、属性和下标比算术运算符绑定得更紧密:```python
x = f(a) + b.c
```在形成加法表达式之前解析调用和属性。
## 19.9 赋值解析
赋值语法比表达式语法受到更多限制。
有效的分配目标包括:```python
x = 1
obj.attr = 1
items[0] = 1
a, b = pair
```无效的分配目标包括:```python
1 = x
a + b = x
f() = x
```解析器必须认识到赋值的左侧不仅仅是任何表达式。它必须是一个有效的目标。
这也会影响增强分配:```python
x += 1
obj.value *= 2
items[i] -= 3
```和带注释的作业:```python
x: int = 1
name: str
```CPython 的解析器和 AST 构建器必须保留用于读取的表达式和用于写入的目标之间的差异。
在 AST 级别,名称带有上下文:```text
Name id="x", ctx=Load
Name id="x", ctx=Store
Name id="x", ctx=Del
```例子:```python
x = x + 1
```左边`x`是`Store`。右边的`x`是`Load`。
## 19.10 歧义和有序选择
PEG 解析使用有序替代项。
考虑多个语法替代方案开头相似的语法。解析器首先尝试一种替代方案。如果成功,则使用该结果。如果失败,可以尝试以后的替代方案。
这为语法作者提供了一种解决歧义的受控方法。
简化示例:```text
small_stmt:
| assignment
| expression
```必须在纯表达式替代方案之前考虑赋值替代方案,因为赋值通常以类似表达式的目标开始。
输入:```python
x = 1
```前缀`x`可以开始一个表达式,但完整的语句是赋值。语法必须有序且结构化,以便作业获胜。
PEG 还支持 CPython 语法符号中的“剪切”运算符。语言参考指出`~`在语法中将其标记为提交当前替代方案的剪切,如果替代方案随后失败,则该规则失败。 ([Python 文档][1])
削减对于性能和诊断很有用。一旦解析器看到足够的标记来知道它正在解析哪个构造,它就可以停止考虑不相关的替代方案。
## 19.11 前瞻
解析器通常需要在决定解析什么之前检查即将到来的标记。
Lookahead 询问:“下一个标记或下一个标记序列是否与此模式匹配?”
例子:```python
x: int
```这可以开始一个带注释的作业。
例子:```python
x + y
```这是一个表达式语句。
两者都以`NAME`。解析器需要比第一个标记更多的上下文。
Lookahead 还有助于尽早拒绝语法并选择更好的错误消息。
简化的前瞻模型:```text
if next token is NAME and following token is ":":
parse annotated assignment
else:
parse expression statement
```真正的 CPython 语法使用更丰富的机制,但心理模型是相同的:某些决策需要提前查看而不永久消耗令牌。
## 19.12 回溯
PEG 解析器可以回溯。
回溯意味着解析器尝试一条语法路径,失败,恢复先前的标记位置,然后尝试另一条路径。
型号示例:```text
try parse async function definition
if it fails:
restore token position
try parse normal function definition
if it fails:
restore token position
try parse simple statement
```回溯使语法创作比严格的单标记先行解析器更加灵活。
但无限制的回溯可能代价高昂。 CPython 的解析器使用语法结构、剪切、记忆和目标规则来保持解析效率。
## 19.13 AST 构建
CPython 中的解析不仅仅接受或拒绝语法。它构建了一个 AST。
示例来源:```python
x = 1 + 2
```AST 形状:```text
Module
Assign
targets:
Name id="x", ctx=Store
value:
BinOp
left: Constant 1
op: Add
right: Constant 2
```在Python级别:```python
import ast
tree = ast.parse("x = 1 + 2\n")
print(ast.dump(tree, indent=4))
```这会产生一个结构化树。
AST 仍然不是字节码。它是一个带有语义注释的语法树,例如加载/存储上下文和源位置。
## 19.14 源位置
AST 节点携带源位置信息。
这包括行号和列偏移量。现代 CPython 还跟踪许多节点的结束位置。
例子:```python
import ast
tree = ast.parse("x = 1 + 2\n")
node = tree.body[0]
print(node.lineno)
print(node.col_offset)
print(node.end_lineno)
print(node.end_col_offset)
```源位置支持:```text
tracebacks
syntax errors
debuggers
coverage tools
profilers
AST tools
editor integrations
```如果没有准确的源位置,CPython 仍然可以执行代码,但诊断和工具质量会差很多。
## 19.15 语法错误
当解析失败时,CPython 会引发`SyntaxError`或子类,例如`IndentationError`。
语法错误应该报告:```text
filename
line number
column offset
source text
message
```例子:```python
if x
pass
```解析器看到`if`语句缺少冒号。
在无法解析的地方附近有一个有用的错误点:```text
SyntaxError: expected ':'
```解析错误并不总是那么简单。由于解析器可能会回溯,因此它必须记住最有用的失败位置,而不仅仅是最终失败的替代方案。
良好的语法错误是解析器工程的主要部分。
## 19.16 无效规则和更好的诊断
CPython 的语法包括专门用于更好的错误消息的规则。
解析器通常可以拒绝错误的语法:```text
SyntaxError: invalid syntax
```但 CPython 经常尝试检测常见错误:```python
if x = 1:
pass
```这可以产生比一般解析失败更有用的消息。
无效的语法规则不打算接受程序。它们用于识别错误的形式并提出更好的诊断。
实用模型:```text
normal grammar accepts valid Python
invalid rules recognize common invalid Python
error machinery reports a targeted message
```这使解析器保持严格,同时改善用户体验。
## 19.17 解析器输出尚未完全验证
成功解析意味着源代码与 Python 语法匹配。这并不意味着该计划在以后的任何意义上都是完全有效的。
一些检查发生在解析之后。
例子:```python
return 1
```在模块级别,这在语法上类似于 return 语句,但它在函数外部无效。
另一个例子:```python
break
```在模块级别,这在语法上是一个break语句,但它在循环之外无效。
这些约束通常在 AST 验证、符号表分析或编译过程中进行检查。
这些阶段是分开的:```text
parser:
Can these tokens form a syntax tree?
later compiler checks:
Is this syntax legal in this context?
Which names are local, global, free, or cell variables?
Can this construct be compiled here?
```## 19.18`ast.parse`公众`ast.parse()`函数在 Python 级别公开 CPython 的解析机制。
例子:```python
import ast
src = """
def add(a, b):
return a + b
"""
tree = ast.parse(src)
print(ast.dump(tree, indent=4))
```这对于:```text
linters
formatters
static analyzers
code generation tools
security scanners
refactoring tools
documentation tools
```但`ast.parse()`返回 AST,而不是令牌,也不是字节码。
对于字节码,请使用`compile()`和`dis`:
```python
import dis
code = compile("x = 1 + 2\n", "<input>", "exec")
dis.dis(code)
```其关系为:```text
tokenize.tokenize() source to tokens
ast.parse() source to AST
compile() source or AST to code object
dis.dis() code object to bytecode display
```## 19.19 解析模式
Python源代码可以用不同的模式进行解析。
常见模式:
|模式|意义|
| -------- | ---------------------------------------------------- |
|`exec`|解析模块或语句序列 |
|`eval`|解析单个表达式 |
|`single`|解析交互式输入 |
例子:```python
compile("x = 1", "<input>", "exec")
compile("1 + 2", "<input>", "eval")
compile("print(1)", "<input>", "single")
eval模式拒绝语句:```python
compile("x = 1", "", "eval")
解析模式决定语法起始符号。
## 19.20 F 字符串和解析器
现代 CPython 通过解析器机制解析 f 字符串表达式。
在 Python 3.12 之前,f 字符串表达式解析具有更多特殊情况行为。 Python 3.12 通过 PEP 701 更改了 f 字符串,将它们更全面地纳入语法中。 Python 3.12 文档指出,f 字符串是使用 PEG 解析器进行解析的,从而允许更精确的错误消息,并且由于这些更改,标记化性能得到了提高。 ([Python 文档][2])
示例:```python
name = "Ada"
text = f"hello {name.upper()}"
```嵌入表达式:```python
name.upper()
```被解析为 Python 表达式语法。
这意味着 f 字符串不仅仅是运行时的字符串插值。它们包含必须在结构上进行解析和表示的嵌套语法。
## 19.21 CPython 中的解析器文件
与解析器相关的重要源代码区域包括:```text
Grammar/python.gram
Parser/
Python/ast.c
Include/internal/
Lib/ast.py
Lib/test/test_grammar.py
Lib/test/test_syntax.py
```确切的文件边界可能会改变,但概念上的分割是稳定的:
|面积 |角色 |
| ---------------- | ------------------------------------------- |
|语法|定义 Python 语法 |
|解析器生成器 |从语法生成解析器实现 |
|解析器运行时 |消耗令牌并应用语法规则 |
| AST 建设 |构建 AST 节点 |
| AST 验证 |检查结构的正确性 |
|测试 |保护语法和诊断 |
阅读CPython时,从语法开始。然后遵循语法规则如何创建 AST 节点。
## 19.22 示例:解析函数
来源:```python
def square(x):
return x * x
```代币级别视图:```text
NAME "def"
NAME "square"
LPAR "("
NAME "x"
RPAR ")"
COLON ":"
NEWLINE
INDENT
NAME "return"
NAME "x"
STAR "*"
NAME "x"
NEWLINE
DEDENT
ENDMARKER
```AST 级别视图:```text
Module
FunctionDef
name: "square"
args:
arguments
arg: "x"
body:
Return
BinOp
left: Name "x", Load
op: Mult
right: Name "x", Load
```解析器创建这个层次结构是因为语法说:```text
a module contains statements
a function definition is a statement
a function body is a block
a return statement can contain an expression
a multiplication expression contains two operands
a name expression loads a value
```每个级别都赋予平面令牌流以意义。
## 19.23 示例:解析`if`声明
来源:```python
if score >= 60:
result = "pass"
else:
result = "fail"
```AST 形状:```text
Module
If
test:
Compare
left: Name "score"
op: GtE
comparator: Constant 60
body:
Assign
target: Name "result", Store
value: Constant "pass"
orelse:
Assign
target: Name "result", Store
value: Constant "fail"
```解析器必须附加`else`到正确的`if`。
缩进标记在这里很有帮助。这`else`出现在`DEDENT`结束第一个块,但缩进级别与`if`。
这是 Python 的布局敏感语法与解析器结构直接交互的地方。
## 19.24 示例:解析推导式
来源:```python
squares = [x * x for x in values if x > 0]
```AST 形状:```text
Assign
target: Name "squares", Store
value:
ListComp
element:
BinOp
Name "x"
Mult
Name "x"
generators:
comprehension
target: Name "x", Store
iter: Name "values", Load
ifs:
Compare
Name "x"
Gt
Constant 0
```推导式是表达式语法,但它们包含类似赋值的目标和嵌套子句。
解析器必须区分:```python
[x * x for x in values]
```来自列表文字:```python
[x * x, y]
```两者都以`[`和一个表情。第一个表达式之后的标记序列决定了解析器构建的结构。
## 19.25 解析器边界
解析器不执行代码。
它不调用函数:```python
f()
```它只构建一个调用节点。
它不导入模块:```python
import os
```它只构建一个导入节点。
它不解析名称:```python
x + y
```它只构建名称和二元运算节点。
它不会评估超出 AST 层表示范围的常量。
解析器的工作是结构化的。后续阶段分配范围、生成字节码并执行操作。
## 19.26 最小心智模型
使用这个模型:```text
The tokenizer emits a flat stream of tokens.
The parser matches that stream against Python grammar.
CPython’s modern parser is PEG-based.
Grammar alternatives are ordered.
The parser builds an AST.
The AST records statements, expressions, contexts, and source locations.
Syntax errors are raised when the token stream cannot form valid grammar.
Some validity checks happen after parsing.
```解析是源文本首先变成树形的地方。此阶段之后的所有内容都适用于结构而不是原始令牌顺序。