"tokenize" --- 对 Python 代码使用的标记解析器
*********************************************

**源码：** Lib/tokenize.py

======================================================================

"tokenize" 模块为 Python 源代码提供了一个词法扫描器，用 Python 实现。
该模块中的扫描器也将注释作为标记返回，这使得它对于实现“漂亮的输出器”非
常有用，包括用于屏幕显示的着色器。

为了简化标记流的处理，所有的 运算符 和 定界符 以及 "Ellipsis" 返回时都
会打上通用的 "OP" 标记。 可以通过 "tokenize.tokenize()" 返回的 *named
tuple* 对象的 "exact_type" 属性来获得确切的标记类型。

警告:

  请注意本模块中的函数被设计为仅能解析符合语法的 Python 代码（当使用
  "ast.parse()" 解析代码时不会引发异常）。 在提供无效的 Python 代码时
  本模块中函数的行为是 **未定义** 的并可能在任何时候发生改变。


对输入进行解析标记
==================

主要的入口是一个 *generator*:

tokenize.tokenize(readline)

   生成器 "tokenize()" 需要一个 *readline* 参数，这个参数必须是一个可
   调用对象，且能提供与文件对象的 "io.IOBase.readline()" 方法相同的接
   口。每次调用这个函数都要 返回字节类型输入的一行数据。

   生成器产生 5 个具有这些成员的元组：令牌类型；令牌字符串；指定令牌在
   源中开始的行和列的 2 元组 "(srow, scol)" ；指定令牌在源中结束的行和
   列的 2 元组 "(erow, ecol)" ；以及发现令牌的行。所传递的行（最后一个
   元组项）是 *实际的* 行。 5 个元组以 *named tuple* 的形式返回，字段
   名是： "type string start end line" 。

   返回的 *named tuple* 有一个额外的属性，名为 "exact_type" ，包含了
   "OP" 标记的确切操作符类型。 对于所有其他标记类型， "exact_type" 等
   于命名元组的 "type" 字段。

   在 3.1 版本发生变更: 增加了对 named tuple 的支持。

   在 3.3 版本发生变更: 添加了对 "exact_type" 的支持。

   根据 **PEP 263** ，"tokenize()" 通过寻找 UTF-8 BOM 或编码 cookie 来
   确定文件的源编码。

tokenize.generate_tokens(readline)

   对读取 unicode 字符串而不是字节的源进行标记。

   和 "tokenize()" 一样， *readline* 参数是一个返回单行输入的可调用参
   数。然而， "generate_tokens()" 希望 *readline* 返回一个 str 对象而
   不是字节。

   其结果是一个产生具名元组的的迭代器，与 "tokenize()" 完全一样。 它不
   会产生 "ENCODING" 标记。

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

提供了另一个函数来逆转标记化过程。这对于创建对脚本进行标记、修改标记流
并写回修改后脚本的工具很有用。

tokenize.untokenize(iterable)

   将令牌转换为 Python 源代码。 *iterable* 必须返回至少有两个元素的序
   列，即令牌类型和令牌字符串。任何额外的序列元素都会被忽略。

   重构的脚本以单个字符串的形式返回。 结果被保证为标记回与输入相匹配，
   因此转换是无损的，并保证来回操作。 该保证只适用于标记类型和标记字符
   串，因为标记之间的间距（列位置）可能会改变。

   它返回字节，使用 "ENCODING" 标记进行编码，这是由 "tokenize()" 输出
   的第一个标记序列。如果输入中没有编码令牌，它将返回一个字符串。

"tokenize()" 需要检测它所标记源文件的编码。它用来做这件事的函数是可用
的：

tokenize.detect_encoding(readline)

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

   它最多调用 readline 两次，并返回所使用的编码（作为一个字符串）和它
   所读入的任何行（不是从字节解码的）的 list 。

   它从 UTF-8 BOM 或编码 cookie 的存在中检测编码格式，如 **PEP 263**
   所指明的。 如果 BOM 和 cookie 都存在，但不一致，将会引发
   "SyntaxError"。 请注意，如果找到 BOM ，将返回 "'utf-8-sig'" 作为编
   码格式。

   如果没有指定编码，那么将返回默认的 "'utf-8'" 编码.

   使用 "open()" 来打开 Python 源文件：它使用 "detect_encoding()" 来检
   测文件编码。

tokenize.open(filename)

   使用由 "detect_encoding()" 检测到的编码，以只读模式打开一个文件。

   在 3.2 版本加入.

exception tokenize.TokenError

   当文件中任何地方没有完成 docstring 或可能被分割成几行的表达式时触发
   ，例如:

      """Beginning of
      docstring

   或者：

      [1,
       2,
       3

注意，未封闭的单引号字符串不会导致错误发生。它们被标记为 "ERRORTOKEN"
，然后是其内容的标记化。


命令行用法
==========

在 3.3 版本加入.

"tokenize" 模块可以作为一个脚本从命令行执行。这很简单:

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

可以接受以下选项：

-h, --help

   显示此帮助信息并退出

-e, --exact

   使用确切的类型显示令牌名称

如果 "filename.py" 被指定，其内容会被标记到 stdout 。否则，标记化将在
stdin 上执行。


例子
====

脚本改写器的例子，它将 float 文本转换为 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)
