资讯详情

资讯详情

深入解析 CPython `tokenize` 模块:Python 源码词法扫描器完全指南

深入解析 CPythontokenize模块Python 源码词法扫描器完全指南【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文围绕 CPython 标准库中的tokenize模块源码位于 Lib/tokenize.py展开全面讲解其作为 Python 源码词法扫描器lexical scanner的核心 API、编码探测机制、命令行用法与无损往返原理。阅读后你将掌握如何用tokenize/generate_tokens把源码拆成带行列坐标的 token 流用untokenize重写并还原源码以及如何基于该模块构建语法高亮、美化打印、代码变换与静态检查等工具并了解其底层如何由内部 C 词法分析器驱动。模块定位Python 源码的词法扫描器tokenize模块为 Python 源代码提供了一个用 Python 实现的词法扫描器。与编译期真正使用的 C 词法分析器相比它有两个显著特点返回注释作为 token。真正的词法分析器会丢弃注释与空白而tokenize会额外产出COMMENT、NL、INDENT、DEDENT、ENCODING、ERRORTOKEN等“非语法”token参见 Lib/token.py 中These arent used by the C tokenizer but are needed for tokenize.py的注释。正因如此它非常适合实现“美化打印器”pretty-printer包括屏幕显示的着色器colorizer——需要先看到注释才能给注释上色。操作符统一为OP。为简化 token 流处理所有操作符与分隔符 token 以及Ellipsis...都统一以通用的token.OP类型返回。若需要精确类型可通过返回命名元组的exact_type属性获得如(对应LPAR、对应EQEQUAL。完整的精确映射表EXACT_TOKEN_TYPES定义在 Lib/token.py。模块导出了token模块的全部常量__all__中额外包含tokenize、generate_tokens、detect_encoding、untokenize、TokenInfo、open、TokenError这些名称Lib/tokenize.py。token常量本身NAME、NUMBER、STRING……由 Grammar/Tokens 中的完整 token 清单生成。重要警告模块级设计约束本模块中的函数只被设计用于解析语法合法的 Python 代码——即那些能被 ast.parse 成功解析而不抛异常的代码。当输入非法代码时本模块各函数的行为是未定义的并且可能在任意版本发生变化。因此不要把本模块当作对任意文本的健壮词法分析器使用。核心数据模型TokenInfo五元组tokenize()与generate_tokens()生成的每个 token 都是一个五元组包含字段含义typetoken 类型见 Lib/token.py 中的整型常量如NAME1、NUMBER2、STRING3stringtoken 字符串本身start起始位置一个(srow, scol)二元组1-based 行、0-based 列end结束位置一个(erow, ecol)二元组linetoken 所在的物理行即源码中的原始行含换行符而不是拼接了续行的逻辑行该五元组以名为TokenInfo的命名元组返回字段名依次为type string start end line定义见 Lib/tokenize.py。命名元组自 Python 3.1 起支持其repr会自动把类型数字注释为人类可读名称例如TokenInfo(type1 (NAME), stringdef, start(1, 0), end(1, 3), linedef say_hello():\n)便于调试。exact_type解开OP的精确身份TokenInfo额外提供了一个属性exact_type自 Python 3.3 起支持实现见 Lib/tokenize.py当type OP且string命中EXACT_TOKEN_TYPES时返回精确操作符类型如(→LPAR、:→COLONEQUAL、...→ELLIPSIS、!→NOTEQUAL其他情况与type字段相同。下表摘自 Lib/token.py 的EXACT_TOKEN_TYPES列出部分典型映射可用于快速查阅token 字符串精确类型常量说明()LPAR/RPAR圆括号[]LSQB/RSQB方括号{}LBRACE/RBRACE花括号:COLON冒号:COLONEQUAL海象运算符!EQEQUAL/NOTEQUAL相等性判断EQUAL赋值-RARROW函数注解箭头...ELLIPSISEllipsis 字面量****DOUBLESTAR/DOUBLESTAREQUAL幂运算////DOUBLESLASH/DOUBLESLASHEQUAL整除AT/ATEQUAL装饰器/矩阵乘法Tokenizing input两个核心入口生成器模块的主入口是一个generator。为支持字节流与文本流两种输入方式提供了两个几乎等价的 APItokenize(readline)按字节 tokenizetokenize()生成器要求一个参数readline一个可调用对象提供与文件对象的 io.IOBase.readline 相同的接口——每次调用返回一行字节bytes到达文件末尾时返回空字节串b。该生成器每步产出一个上文描述的 5 元组命名元组。其额外约定是产出的第一个 token 一定是ENCODINGtoken值为用于解码字节流的编码名从源码可见 Lib/tokenize.py先调用detect_encoding探测编码并预读行若编码为utf-8-sig则 BOM 已被剥离实际按utf-8上报。tokenize通过查找 UTF-8 BOM 或编码声明coding cookie来确定源文件编码符合 PEP 263。generate_tokens(readline)直接 tokenize Unicode 文本generate_tokens()与tokenize()的 API 完全相同区别在于它期望readline返回str对象而非 bytes即源码已按正确编码解码好。其结果为与tokenize()完全相同的命名元组迭代器但不会产出ENCODINGtoken。两个入口在 CPython 主分支的实现都收敛于内部函数_generate_tokens_from_c_tokenizerLib/tokenize.py该函数把 readline 调用对象或编码传给内置_tokenize模块的TokenizerIterC 迭代器逐条把内部 token 信息组装为TokenInfo捕获到的底层SyntaxError会经_transform_msg归一化后重新抛出为TokenError。也就是说当前版本中的模块是用 Python 薄封装包住内部 C 词法分析器实现的。编码探测detect_encoding与opendetect_encoding(readline)tokenize()需要探测被处理源文件的编码此功能独立暴露为detect_encoding()。它接受与tokenize()相同的readline参数返回(encoding, lines)encoding探测出的编码名字符串lines已读入但尚未解码的字节行列表供调用方复用避免重复读取。其判定规则实现见 Lib/tokenize.py最多调用readline两次。依据 PEP 263编码 cookie 只可能出现在第 1 行或第 1 行为空/仅注释时出现在第 2 行故读取前两行足矣若某行以 UTF-8 BOMb\xef\xbb\xbf开头则剥离 BOM 并按 BOM 处理。BOM 存在返回编码为utf-8-sig。cookie 存在通过正则cookie_re^[ \t\f]*#.*?coding[:][ \t]*([-\w.])匹配形如# -*- coding: latin-1 -*-的声明编码名会经_get_normal_name归一化如utf_8→utf-8只关心前 12 个字符行为对齐 Parser/tokenizer/helpers.c 的实现再经codecs.lookup校验是否为合法编码。BOM 与 cookie 同时存在但冲突cookie 声明非 utf-8抛出 SyntaxError。未声明任何编码返回默认值utf-8。另外源码行若包含 NUL 字节会报SyntaxError(source code cannot contain null bytes)编码名未知会报SyntaxError(unknown encoding: ...)——这两条规则“模仿 Python 解释器的行为”源码注释原文保证探测结果与解释器一致。open(filename)按探测编码打开源码文件普通open()默认按 locale 编码打开文本文件处理带# -*- coding: -*-声明的文件时可能解码出错。因此模块提供Python 3.2 起tokenize.open()以只读方式打开文件并自动使用detect_encoding探测到的编码进行解码实现见 Lib/tokenize.py先以二进制打开读取前几行完成探测seek(0)后包一层TextIOWrappermode属性被置为r。读取 Python 源码文件应优先使用tokenize.open。TokenError当“可能跨越多行的 docstring 或表达式”在文件任何位置都未闭合时抛出TokenError。典型场景Beginning of docstring[1, 2, 3即未闭合的三引号字符串或括号表达式一直延续到文件末尾。注意源码中_generate_tokens_from_c_tokenizer会把内部 C tokenizer 报的语法类错误统一转成TokenError并把 unterminated triple-quoted string literal 这类信息转译为兼容旧版的EOF in multi-line string见 Lib/tokenize.py。untokenize把 token 流还原为源码模块还提供了反转 tokenization 过程的函数untokenize(iterable)用于实现“先 tokenize 脚本 → 修改 token 流 → 写回修改后脚本”的工具链。输入的 iterable 中每个元素必须是至少包含两个元素的序列token 类型与 token 字符串额外的元素如 5 元组中的坐标与行文本会被忽略。因此 5 元组流与精简的 2 元组流都能直接喂给untokenize。无损往返保证结果保证能再次 tokenize 出与输入匹配的 token因而转换是 lossless 的、round-trip 可复现。该保证仅针对“token 类型 token 字符串”token 之间的空白列位置可能被规范化改变。返回类型若输入流中存在ENCODINGtoken即来自tokenize()的字节流输出它是第一个 token则返回字节串并使用该编码编码若无编码 token例如来自generate_tokens的流或手写 2 元组返回str。实现上由Untokenizer类完成Lib/tokenize.py其细节包括用栈追踪INDENT/DEDENT还原缩进用add_backslash_continuation在行号跳变且无换行 token 时补回反斜杠续行符对FSTRING_MIDDLE/TSTRING_MIDDLE中需要保留的花括号做escape_brackets转义处理兼容旧的 2 元素compat输入格式并自动在相邻字符串、相邻花括号之间插入空格以保持可解析性。Lib/test/test_tokenize.py中的TestRoundtripLib/test/test_tokenize.py系统验证了这一保证把同一段源码分别以 5 元组和 2 元组 tokenize再untokenize后重新 tokenize断言前后 token 对完全一致在不含歧义反斜杠的普通源码上untokenize(tokenize(code))还必须与去掉 BOM 后的原始字节逐字节相等。命令行用法自 Python 3.3 起模块可以作为脚本从命令行执行python -m tokenize [-e] [filename.py]支持的选项选项说明-h,--help显示帮助信息并退出-e,--exact使用精确类型显示 token 名称若指定了filename.py则对其内容做 tokenize 并输出到 stdout否则对stdin执行 tokenize。传入管道内容同样可行例如echo print(1) | python -m tokenize。自 Python 3.15 起CLI默认输出彩色可借助环境变量控制规则见 Doc/using/cmdline.rst 的 “Controlling color” 一节设置TERMdumb禁用颜色设置FORCE_COLOR强制启用颜色适合非终端但能显示 ANSI 转义的 CI 系统设置NO_COLOR禁用所有颜色优先于FORCE_COLOR仅控制 Python 解释器自身的颜色可使用PYTHON_COLORS环境变量其优先级最高PYTHON_COLORSNO_COLORFORCE_COLOR。CLI 内部经由 Lib/tokenize.py 的_main()实现按文件/标准输入收集全部 token 后由_format_tokens用_get_token_colors中的主题配色为COMMENT、NUMBER、STRING、OP、SOFT_KEYWORD、NAME、NEWLINE等各类 token 上色配色主题来自Lib/_colorize.py输出统一为“坐标区间右对齐 token 名 token 值”的三段式布局若 tokenize 过程发生IndentationError、TokenError、SyntaxError、OSError会以文件:行:列: error: 消息的格式输出到 stderr 并以非零码退出。输出示例对下面的脚本def say_hello(): print(Hello, World!) say_hello()执行python -m tokenize hello.py输出如下第一列为 token 所在的行/列坐标范围第二列为 token 名称最后一列为 token 值$ python -m tokenize hello.py 0,0-0,0: ENCODING utf-8 1,0-1,3: NAME def 1,4-1,13: NAME say_hello 1,13-1,14: OP ( 1,14-1,15: OP ) 1,15-1,16: OP : 1,16-1,17: NEWLINE \n 2,0-2,4: INDENT 2,4-2,9: NAME print 2,9-2,10: OP ( 2,10-2,25: STRING Hello, World! 2,25-2,26: OP ) 2,26-2,27: NEWLINE \n 3,0-3,1: NL \n 4,0-4,0: DEDENT 4,0-4,9: NAME say_hello 4,9-4,10: OP ( 4,10-4,11: OP ) 4,11-4,12: NEWLINE \n 5,0-5,0: ENDMARKER 注意观察几个关键 token行 1 的ENCODING记录了解码编码函数体前有INDENT、结束后有DEDENT空行以NL而非NEWLINE标记NEWLINE是逻辑行结束、NL是“不改变逻辑结构”的普通换行文件末尾以空字符串值的ENDMARKER收尾。使用-e选项可以把OP展开为精确类型例如()显示为LPAR/RPAR、:显示为COLON$ python -m tokenize -e hello.py 0,0-0,0: ENCODING utf-8 1,0-1,3: NAME def 1,4-1,13: NAME say_hello 1,13-1,14: LPAR ( 1,14-1,15: RPAR ) 1,15-1,16: COLON : 1,16-1,17: NEWLINE \n 2,0-2,4: INDENT 2,4-2,9: NAME print 2,9-2,10: LPAR ( 2,10-2,25: STRING Hello, World! 2,25-2,26: RPAR ) 2,26-2,27: NEWLINE \n 3,0-3,1: NL \n 4,0-4,0: DEDENT 4,0-4,9: NAME say_hello 4,9-4,10: LPAR ( 4,10-4,11: RPAR ) 4,11-4,12: NEWLINE \n 5,0-5,0: ENDMARKER 实战示例示例一脚本重写器——把 float 字面量替换为 Decimal 对象经典用法是“tokenize → 修改 token 流 → untokenize 写回”。下面的decistmt演示了如何把语句字符串中的浮点数字面量包装为Decimal(...)调用from tokenize import tokenize, untokenize, NUMBER, STRING, NAME, OP from io import BytesIO def decistmt(s): Substitute Decimals for floats in a string of statements. from decimal import Decimal s print(21.3e-5*-.1234/81.7) decistmt(s) print (Decimal (21.3e-5)*-Decimal (.1234)/Decimal (81.7)) The format of the exponent is inherited from the platform C library. Known cases are e-007 (Windows) and e-07 (not Windows). Since were only showing 12 digits, and the 13th isnt close to 5, the rest of the output should be platform-independent. exec(s) #doctest: ELLIPSIS -3.21716034272e-0...7 Output from calculations with Decimal should be identical across all platforms. exec(decistmt(s)) -3.217160342717258261933904529E-7 result [] g tokenize(BytesIO(s.encode(utf-8)).readline) # tokenize the string for toknum, tokval, _, _, _ in g: if toknum NUMBER and . in tokval: # replace NUMBER tokens result.extend([ (NAME, Decimal), (OP, (), (STRING, repr(tokval)), (OP, )) ]) else: result.append((toknum, tokval)) return untokenize(result).decode(utf-8)实现要点BytesIO(...).readline让tokenize()可以从内存字节串逐行读取对每个命中“含小数点”的NUMBERtoken替换为Decimal(原字面量)这组 tokenNAMEOPSTRINGOP其余 token 原样保留最后untokenize负责把INDENT/DEDENT、行尾、括号等结构还原为可执行代码再.decode(utf-8)得到文本。示例二程序化读取文本流generate_tokenstokenize.open若源码文件带有非 UTF-8 编码声明正确的读法是先用tokenize.open按声明编码打开再配合处理str的generate_tokensimport tokenize with tokenize.open(hello.py) as f: tokens tokenize.generate_tokens(f.readline) for token in tokens: print(token)若已确定文件是 UTF-8 且希望读取原始字节会产生ENCODING头 token则直接用二进制文件对象的readline配合tokenizeimport tokenize with open(hello.py, rb) as f: tokens tokenize.tokenize(f.readline) for token in tokens: print(token)两段代码的差别正是文档反复强调的tokenize面向bytes自己负责 PEP 263 编码探测产出ENCODING首 tokengenerate_tokens面向已解码的str不产出ENCODING。前者更接近解释器行为、适合处理任意编码的磁盘文件后者适合处理内存中已是文本的源码如来自编辑器、网络或拼接字符串。结合源码看底层实现阅读 Lib/tokenize.py 可更深入理解模块机制顶层正则Lib/tokenize.py曾是该模块的核心手工词法规则定义了数字十六进制0[xX]...、二进制、八进制、十进制、浮点、复数虚数、字符串前缀与各类引号字符串、操作符按长度降序排序避免抢在之前匹配、Ignore Whitespace 续行 maybe(Comment)等模式其中Whitespace、Comment、Ignore、Name等命名仍保留在源码顶部tabsize 8说明缩进中 tab 按 8 列展开。当前版本的真实驱动是内部 C 词法分析器自引入内部_tokenize模块后tokenize/generate_tokens都把源文本交给_tokenize.TokenizerIter(source, encoding..., extra_tokensTrue)迭代再用TokenInfo._make(info)包装产出Lib/tokenize.py。extra_tokensTrue正是请求 C 词法分析器补发COMMENT/NL/INDENT/DEDENT/ENCODING等额外 token 的开关。可以推断该设计使纯 Python 词法规则与编译期 C 词法分析器保持同步避免了两套词法规则长期分叉的问题。与ast的关系tokenize位于“词法”层次产出的是扁平的 token 流ast 模块在其之上做语法分析产出语法树。模块警告“只解析语法合法代码”正是因为底层 C tokenizer 面向编译路径、假定输入必然能通过ast.parse。测试验证模块行为在 Lib/test/test_tokenize.py约 3795 行中有系统覆盖主要测试类包括TokenizeTestLib/test/test_tokenize.py与继承它的GenerateTokensTestLib/test/test_tokenize.py验证字节流与文本流两种入口产出的 token 序列TestTokenizerAdheresToPep0263Lib/test/test_tokenize.py对照 PEP 263 验证各编码声明文件如带latin-1cookie 却带 UTF-8 BOM 的文件必须抛SyntaxErrorTestDetectEncodingLib/test/test_tokenize.py无 BOM/无 cookie、仅 BOM、仅 cookie、非法 cookie 等探测组合UntokenizeTestLib/test/test_tokenize.py与TestRoundtripLib/test/test_tokenize.py验证untokenize的往返无损性InvalidPythonTestsLib/test/test_tokenize.py、CTokenizeTestLib/test/test_tokenize.py与CommandLineTestLib/test/test_tokenize.py分别覆盖非法输入、C 驱动路径与python -m tokenize命令行行为。需要深入开发 token 级工具如自定义 linter、格式化器时阅读这些测试是快速理解各类 token 在真实源码中形态的最佳途径。适用前提与限制小结tokenize/generate_tokens只接受语法合法的 Python 源码对非法代码行为未定义tokenize()返回字节解码所需编码信息ENCODING首 tokengenerate_tokens()不返回处理磁盘上的源码文件请用tokenize.open以正确解码若文件可能非 UTF-8务必走tokenize字节流 探测而不是先把字节decode(utf-8)再走generate_tokensuntokenize保证“类型 值”无损往返但不保证保留空白布局故不适合做“保持原格式的精美排版”工具的基础——它更适合 token 级改写场景改写后通常需配合格式化器重排。掌握了上述 API 与约束即可基于tokenize构建语法高亮、.py源码统计、注释提取、代码现代化改写等各类工具且在需要时能直接阅读 Lib/tokenize.py 与 Lib/token.py 深入到 token 级语义。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →