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)