tokenize — Python 源代码的词法分析器

源代码: Lib/tokenize.py


tokenize 模块为 Python 源代码提供了一个词法扫描器,它用 Python 实现。此模块中的扫描器也会将注释作为 token 返回,使其对于实现“漂亮打印机”,包括屏幕显示的着色器非常有用。

为了简化 token 流处理,所有运算符分隔符 token 以及 Ellipsis 都使用通用的 OP token 类型返回。可以通过检查从 tokenize.tokenize() 返回的命名元组上的 exact_type 属性来确定确切的类型。

警告

请注意,此模块中的函数仅用于解析语法上有效的 Python 代码(使用 ast.parse() 解析时不会引发错误的代码)。当提供无效的 Python 代码时,此模块中函数的行为是未定义的,并且可能会随时更改。

输入词法分析

主要入口点是一个生成器

tokenize.tokenize(readline)

tokenize() 生成器需要一个参数 readline,它必须是一个可调用对象,该对象提供与文件对象的 io.IOBase.readline() 方法相同的接口。对该函数的每次调用都应以字节形式返回一行输入。

生成器产生 5 元组,其中包含以下成员:token 类型;token 字符串;一个 2 元组 (srow, scol),其中整数指定 token 在源代码中开始的行和列;一个 2 元组 (erow, ecol),其中整数指定 token 在源代码中结束的行和列;以及找到 token 的行。传递的行(最后一个元组项)是物理行。5 元组以命名元组的形式返回,其字段名称为:type string start end line

返回的命名元组具有一个名为 exact_type 的附加属性,其中包含 OP token 的确切运算符类型。对于所有其他 token 类型,exact_type 等于命名元组 type 字段。

在 3.1 版本中更改: 添加了对命名元组的支持。

在 3.3 版本中更改: 添加了对 exact_type 的支持。

tokenize() 通过查找 UTF-8 BOM 或编码 cookie,根据 PEP 263 确定文件的源代码编码。

tokenize.generate_tokens(readline)

词法分析源代码,读取的是 Unicode 字符串而不是字节。

tokenize() 一样,readline 参数是一个可调用对象,它返回单行输入。但是,generate_tokens() 期望 readline 返回一个 str 对象而不是字节。

结果是一个迭代器,产生命名元组,与 tokenize() 完全一样。它不产生 ENCODING token。

来自 token 模块的所有常量也从 tokenize 导出。

提供了另一个函数来反转词法分析过程。这对于创建词法分析脚本、修改 token 流以及写回修改后的脚本的工具非常有用。

tokenize.untokenize(iterable)

将 token 转换回 Python 源代码。 iterable 必须返回至少包含两个元素的序列,即 token 类型和 token 字符串。任何附加的序列元素都将被忽略。

重建的脚本以单个字符串返回。结果保证可以重新进行词法分析,以匹配输入,以便转换是无损的,并且可以确保往返。该保证仅适用于 token 类型和 token 字符串,因为 token 之间的间距(列位置)可能会更改。

它返回字节,使用 ENCODING token 进行编码,这是 tokenize() 输出的第一个 token 序列。如果输入中没有编码 token,它将返回一个 str。

tokenize() 需要检测它进行词法分析的源文件的编码。它用于执行此操作的函数可用

tokenize.detect_encoding(readline)

detect_encoding() 函数用于检测应该用于解码 Python 源代码文件的编码。它需要一个参数 readline,其方式与 tokenize() 生成器相同。

它最多会调用 readline 两次,并返回使用的编码(作为字符串)和它已读取的任何行(未从字节解码)的列表。

它从 UTF-8 BOM 或 PEP 263 中指定的编码 cookie 的存在情况来检测编码。如果同时存在 BOM 和 cookie,但它们不一致,则会引发 SyntaxError。请注意,如果找到 BOM,将返回 'utf-8-sig' 作为编码。

如果没有指定编码,则会返回默认的 'utf-8'

使用 open() 打开 Python 源代码文件:它会使用 detect_encoding() 来检测文件编码。

tokenize.open(filename)

使用 detect_encoding() 检测到的编码,以只读模式打开文件。

3.2 版本新增。

exception tokenize.TokenError

当文档字符串或可能跨越多行的表达式在文件中任何地方都未完成时引发,例如

"""Beginning of
docstring

或者

[1,
 2,
 3

命令行用法

3.3 版本新增。

tokenize 模块可以作为脚本从命令行执行。 它非常简单,就像

python -m tokenize [-e] [filename.py]

接受以下选项

-h, --help

显示此帮助信息并退出

-e, --exact

使用确切的类型显示标记名称

如果指定了 filename.py,则其内容将被标记化并输出到 stdout。 否则,将在 stdin 上执行标记化。

示例

将浮点文字转换为 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
    we're only showing 12 digits, and the 13th isn't 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')

从命令行进行标记化的示例。脚本

def say_hello():
    print("Hello, World!")

say_hello()

将被标记化为以下输出,其中第一列是找到标记的行/列坐标范围,第二列是标记的名称,最后一列是标记的值(如果有)

$ 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      ''

可以使用 -e 选项显示确切的标记类型名称

$ 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      ''

以编程方式标记文件的示例,使用 generate_tokens() 读取 Unicode 字符串而不是字节

import tokenize

with tokenize.open('hello.py') as f:
    tokens = tokenize.generate_tokens(f.readline)
    for token in tokens:
        print(token)

或者使用 tokenize() 直接读取字节

import tokenize

with open('hello.py', 'rb') as f:
    tokens = tokenize.tokenize(f.readline)
    for token in tokens:
        print(token)