语法树之前还有一步:词法分析。tokenize 模块能让你看到这一步的原始产物——每个 token 的类型、文本、起止位置。这篇把它从头到尾摊开:注释去哪了、缩进怎么变成 token、f-string 怎么被拆开、哪些错误发生在这一步。
源码 total = price * 3 # 加税
tokenize.generate_tokens(...) NAME 'total' 起 (1, 0) 止 (1, 5) OP '=' 起 (1, 6) 止 (1, 7) NAME 'price' 起 (1, 8) 止 (1, 13) OP '*' 起 (1, 14) 止 (1, 15) NUMBER '3' 起 (1, 16) 止 (1, 17) COMMENT '# 加税' 起 (1, 19) 止 (1, 23) NEWLINE '\n' 起 (1, 23) 止 (1, 24) ENDMARKER '' 起 (2, 0) 止 (2, 0)
每个 token 带有类型、字符串、起止行列。空白不产生 token(只体现在位置上);行末的 NEWLINE 标志一条逻辑语句结束;ENDMARKER 标志文件结束。注释是一个 COMMENT token——它出现在 tokenize 里,却不会出现在语法树里(见下面验证三)。
NAME('if') NAME('x') OP(':') NEWLINE INDENT NAME('y') OP('=') NUMBER('1') NEWLINE NAME('if') NAME('z') OP(':') NEWLINE INDENT NAME('w') OP('=') NUMBER('2') NEWLINE DEDENT DEDENT NAME('v') OP('=') NUMBER('3') NEWLINE ENDMARKERPython 不用花括号,块结构由缩进决定。词法分析器为此维护一个缩进栈:某行缩进比栈顶深,输出一个 INDENT;变浅,就输出一个(或几个)DEDENT。上面最后的 w = 2 之后连续两个 DEDENT,是因为下一行 v = 3 直接退回了最外层,要一口气弹出两层。
COMMENT token: ['# 加税'] ast.dump 里能找到注释吗? False
注释在 tokenize 里有,在 ast 里没有(ast.dump 里找不到「加税」)。所以需要保留注释和原始格式的工具(格式化、重构)要在 tokenize 层工作。另外,def、return 这样的关键字和普通变量一样,在 tokenize 里都是 NAME;是不是关键字要靠 keyword 模块判断。软关键字(match、case、_、type)在别处可以当变量名用,更没法在这一层区分。
[('FSTRING_START', 'f"'), ('FSTRING_MIDDLE', 'a'), ('OP', '{'), ('NAME', 'x'), ('OP', '+'), ('NUMBER', '1'), ('OP', '}'), ('FSTRING_MIDDLE', 'b'), ('FSTRING_END', '"'), ('NEWLINE', '\n'), ('ENDMARKER', '')]本机这版 Python 里 f"a{x + 1}b" 不再是一个整体的 STRING,而是 FSTRING_START、FSTRING_MIDDLE、花括号里的普通 token(x、+、1)、再一个 FSTRING_MIDDLE、FSTRING_END。所以花括号里的表达式和普通代码用同一套规则切分。
['NAME', 'OP', 'OP', 'NUMBER', 'OP', 'NL', 'NUMBER', 'OP', 'NEWLINE', 'NAME', 'OP', 'NUMBER', 'NEWLINE', 'ENDMARKER']
NL(非逻辑换行)与 NEWLINE(逻辑行结束)出现次数: Counter({'NEWLINE': 2, 'NL': 1})括号没闭合时的换行是 NL(非逻辑换行),不是 NEWLINE。两行文字里 x = (1 +\n 2) 只产生一个 NEWLINE,加上后面 y = 3 的一个,共 2 个;NL 是 1 个。这也是为什么在括号里可以随意折行。
"x = 'abc\n": TokenError: ('unterminated string literal (detected at line 1)', (1, 5))
compile: SyntaxError: unterminated string literal (detected at line 1)
'x = (1 + 2\n': TokenError: ('unexpected EOF in multi-line statement', (1, 0))
compile: SyntaxError: '(' was never closed
'if x:\n a = 1\n b = 2\n': tokenize 没报错
compile: IndentationError: unexpected indent字符串没闭合、括号没闭合,tokenize 直接抛 TokenError,compile 报的是 SyntaxError,两者位置说法略有不同。有意思的是最后一条:缩进不一致的那段代码 tokenize 完全没报错,compile 才报 IndentationError——tokenize 模块和真正的编译器词法分析并非一回事,它是给工具用的宽松版本。用它做静态检查时要记住这一点。
json/decoder.py 364 行: {'NAME': 722, 'OP': 703, 'NEWLINE': 222, 'STRING': 73, 'INDENT': 72, 'DEDENT': 72, 'NL': 58, 'NUMBER': 53, 'COMMENT': 15}364 行的 json/decoder.py:NAME 和 OP 加起来占了绝大多数,72 对 INDENT/DEDENT 说明有 72 个缩进块,15 个注释。
tokenize、token、keyword、asttokenize.tokenize)与编码声明