"tokenize" --- Python 源代码的分词器
************************************

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

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

"tokenize" 模块为 Python 源代码提供了一个用 Python 实现的词法扫描器。
该模块中的扫描器还会将注释作为标记返回，这使得它可以用于实现"美化打印
器"，包括用于屏幕显示的代码着色器。

To simplify token stream handling, all operator and delimiter tokens
and "Ellipsis" are returned using the generic "OP" token type. The
"TokenInfo.exact_type" property gives the exact token type.

警告:

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


对输入进行分词
==============

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

tokenize.tokenize(readline)

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

   The generator produces "TokenInfo" objects.

   在 3.1 版本发生变更: Changed to yield "TokenInfo" objects instead
   of plain tuples.

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

tokenize.generate_tokens(readline)

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

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

   The result is an iterator yielding "TokenInfo" objects, exactly
   like "tokenize()". It does not yield an "ENCODING" token.

class tokenize.TokenInfo(type, string, start, end, line)

   A *named tuple* describing a token. It has five fields and an
   additional "exact_type" property.

   Added in version 3.1.

   type

      An integer identifying the token type, as defined by the
      constants in "token", such as "NAME" and "STRING". Operators and
      delimiters have type "OP"; use "exact_type" for their specific
      token type.

   string

      The token text as a "str". For "STRING" tokens, this includes
      the quotes, any prefix, and escape sequences, without evaluating
      them.

      For an "ENCODING" token, this is the encoding name. Tokens
      inserted without corresponding source text can have an empty
      string. In "FSTRING_MIDDLE" and "TSTRING_MIDDLE" tokens, doubled
      braces are replaced by single braces.

   start

      A 2-tuple "(srow, scol)" of ints specifying the inclusive start
      position of the token in the source. Rows are numbered from 1
      and columns from 0. Columns count Unicode code points; a tab
      counts as one column, without expanding it to a tab stop.

      For an "ENCODING" token, this is "(0, 0)".

   end

      A 2-tuple "(erow, ecol)" of ints specifying the exclusive end
      position of the token. Rows are numbered from 1 and columns from
      0. For a token spanning multiple lines, *erow* is the final row
      and *ecol* is the column on that row.

      For example, the name "spam" at the beginning of the first line
      has "start" equal to "(1, 0)" and "end" equal to "(1, 4)".

      For an "ENCODING" token, this is "(0, 0)". A "NEWLINE" or "NL"
      token inserted at the end of input without a final newline can
      have an end column one beyond the source text.

   line

      A "str" containing the full source line on which the token was
      found, including surrounding text and the line terminator if
      present. For a token spanning multiple lines, this can contain
      several source lines. It is empty for "ENCODING" and "ENDMARKER"
      tokens.

   exact_type

      An integer identifying the specific operator type for "OP"
      tokens, as defined by the constants in "token". For all other
      token types, this equals "type". For example, a "+" token has
      "type" equal to "OP" and "exact_type" equal to "PLUS".

      Added in version 3.3.

"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()" 检测到的编码，以只读模式打开一个文件。

   Added in version 3.2.

exception tokenize.TokenError

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

      """Beginning of
      docstring

   或者:

      [1,
       2,
       3


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

Added in version 3.3.

"tokenize" 模块可以作为脚本从命令行执行。 只需简单地：

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

可以接受以下选项：

-h, --help

   显示此帮助信息并退出

-e, --exact

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

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

Added in version 3.15: 输出默认为彩色并且可以 使用环境变量来控制。


例子
====

脚本改写器的例子，它将 float 字面值转换为 Decimal 对象:

   from tokenize import tokenize, untokenize, NUMBER, STRING, NAME, OP
   from io import BytesIO

   def decistmt(s):
       """在一个语句字符串中用 Decimal 替代浮点数。

       >>> from decimal import Decimal
       >>> s = 'print(+21.3e-5*-.1234/81.7)'
       >>> decistmt(s)
       "print (+Decimal ('21.3e-5')*-Decimal ('.1234')/Decimal ('81.7'))"

       指数的格式继承自平台的 C 库。
       已知用例有 "e-007" (Windows) 和 "e-07" (非 Windows)。
       由于我们只显示 12 个数位，且第 13 位的值不到 5，因此
       输出的其余部分应当是不依赖于具体平台的。

       >>> exec(s)  #doctest: +ELLIPSIS
       -3.21716034272e-0...7

       使用 Decimal 进行计算的输出应当在所有平台上保持一致。

       >>> exec(decistmt(s))
       -3.217160342717258261933904529E-7
       """
       result = []
       g = tokenize(BytesIO(s.encode('utf-8')).readline)  # 对字符串分词
       for toknum, tokval, _, _, _ in g:
           if toknum == NUMBER and '.' in tokval:  # 替换 NUMBER 词元
               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)
