未翻訳のページ
このページはまだ日本語に翻訳されていないため、英語の原文を表示しています。 翻訳に協力する
tokenize Module Complexity¶
The tokenize module lexes Python source into TokenInfo tuples. Its two
generators pull one line at a time through the readline callable they are
given and yield each token as soon as the line holding it has been read, so
the cost of tokenizing is linear in the source and the memory it holds is a
line, or one token where a token spans lines, however long the file. untokenize() is the reverse: it writes source
back from tokens and, given full five-tuples, rebuilds the spacing between
them from their positions. Encoding detection reads at most two lines.
Complexity Reference¶
Size variables: n = characters of source; t = tokens yielded; L = characters
in the longest line, or in the longest token when one spans lines, such as a
triple-quoted string; h = bytes in the lines detect_encoding() reads, at
most two; c = characters in the source untokenize() produces.
Tokenizing¶
| Operation | Time | Space | Notes |
|---|---|---|---|
generate_tokens(readline) |
O(n) | O(L) | Lazy. Calls readline for one line at a time, never ahead of the token being yielded, and drops each line once the next begins unless a token spans them. Stopping after k tokens costs the lines read to reach them |
tokenize(readline) |
O(n) | O(L) | detect_encoding() first, then decodes each line as it is read. The first token yielded is ENCODING |
TokenInfo fields, exact_type |
O(1) | O(1) | Tokens on one line share one line string, so a list of every token holds each line once: O(t + n), not O(t·L) |
TokenError |
— | — | Raised from inside the stream, so the tokens of the lines before the fault have already been yielded. Up to 3.11 it is raised only for an EOF inside a string, a bracket or a backslash continuation, and a malformed token is yielded as ERRORTOKEN with tokenizing continuing; from 3.12 an unterminated string or a NUL byte raises it too. An inconsistent dedent raises IndentationError instead, on every version |
tok_name |
O(1) | O(1) | Token type to its name; exact_type names the operator itself |
From 3.12 an f-string is tokenized into its parts, FSTRING_START, an
FSTRING_MIDDLE per run of literal text, the expression's own tokens and
FSTRING_END, where 3.10 and 3.11 yield one STRING; 3.14 does the same for
t-strings. That raises t for the same n.
Encodings¶
| Operation | Time | Space | Notes |
|---|---|---|---|
detect_encoding(readline) |
O(h) | O(h) | Reads the first line, and the second only when the first is blank or a comment without a cookie. Decodes what it read to validate the codec, and returns those lines so the caller need not read them again |
open(filename) |
O(h) | O(h) | detect_encoding() on the file, then seeks back to the start and wraps it in a text stream with that encoding. Nothing else is read until the caller reads |
Reversing¶
| Operation | Time | Space | Notes |
|---|---|---|---|
untokenize(iterable) |
O(t + c) | O(t + c) | Five-tuples: the whitespace between tokens is regenerated from their positions, so a gap of k columns costs k spaces and adjacent tokens cost none. Two-tuples: positions are gone and the spacing is the module's own. Returns bytes when the stream starts with ENCODING, str otherwise |
python -m tokenize [-e] [file] |
as tokenize() |
O(t + n) with a file | A file argument lists every token, and with them every line, before printing the first, so a source that fails prints nothing but the error. Standard input streams, and a fault prints the tokens before it |
Reading stays one line ahead¶
generate_tokens() reads nothing until it is first advanced, and then only
the line holding the next token; tokenize() adds the line or two that settle
the encoding. Scanning for one name near the top of a file costs the lines up
to it, not the file. A list of every token is larger than
the tokens: each carries its line, but the tokens on one line carry the same
string, so the list holds each line once.
import io
import tokenize
source = "first = 1\nsecond = 2\n" * 1000
stream = io.StringIO(source)
calls = 0
def readline():
global calls
calls += 1
return stream.readline()
tokens = tokenize.generate_tokens(readline) # nothing read yet - O(1)
assert calls == 0
first = next(tokens) # reads line 1 and no further - O(L)
assert first.string == "first" and calls == 1
# Stop early and pay for the lines read, not the source - O(their characters)
for token in tokens:
if token.string == "second":
break
assert calls == 2
# Every token on a line shares one line string - O(t + n) for the whole list
every = list(tokenize.generate_tokens(io.StringIO("total = a + b\n").readline))
assert all(token.line is every[0].line for token in every[:-1])
assert every[-1].type == tokenize.ENDMARKER and every[-1].line == ""
Encoding detection reads at most two lines¶
detect_encoding() stops at the first line that settles the question: a
coding cookie, or any line that is neither blank nor a comment. A byte-order
mark alone does not settle it, because a cookie on the second line could
still contradict it. Only when the first line settles nothing is a second
read, and never a third. tokenize.open() runs the same detection and then
rewinds, so the text stream it returns starts at the first byte.
import io
import pathlib
import tempfile
import tokenize
def counting(data):
stream = io.BytesIO(data)
calls = []
def readline():
calls.append(1)
return stream.readline()
return readline, calls
body = b"x = 1\n" * 10_000
readline, calls = counting(b"# -*- coding: latin-1 -*-\n" + body)
encoding, consumed = tokenize.detect_encoding(readline) # one line - O(h)
assert (encoding, len(consumed), len(calls)) == ("iso-8859-1", 1, 1)
readline, calls = counting(b"#!/usr/bin/env python\n# coding: latin-1\n" + body)
encoding, consumed = tokenize.detect_encoding(readline) # two lines - O(h)
assert (encoding, len(consumed), len(calls)) == ("iso-8859-1", 2, 2)
readline, calls = counting(b"import os\n" + body)
encoding, consumed = tokenize.detect_encoding(readline) # code on line 1: stops there
assert (encoding, len(calls)) == ("utf-8", 1)
with tempfile.TemporaryDirectory() as tmp:
path = pathlib.Path(tmp) / "latin.py"
path.write_bytes(b"# coding: latin-1\nname = '\xe9'\n")
with tokenize.open(path) as file: # detects, then seeks back - O(h)
assert file.encoding == "iso-8859-1"
assert file.read() == "# coding: latin-1\nname = '\u00e9'\n"
Spacing comes from positions¶
Given the five-tuples the generators yield, untokenize() places each token
at its recorded column and fills the gap with spaces, so the spaces between
the tokens on a line come back as they were, a tab between two tokens comes
back as one space, and dropping a token leaves its gap behind. Given two-tuples of type
and string it has no positions, and separates tokens by rules of its own.
import io
import tokenize
source = "if x:\n y = 1 # note\n"
tokens = list(tokenize.generate_tokens(io.StringIO(source).readline))
# Five-tuples: the spacing is rebuilt from the recorded columns - O(t + c)
assert tokenize.untokenize(tokens) == source
# Dropping a token keeps the others' columns, and the gap it leaves - O(c)
kept = [token for token in tokens if token.type != tokenize.COMMENT]
assert tokenize.untokenize(kept) == "if x:\n y = 1 \n"
# Two-tuples: the positions are gone, so the spacing is the module's own
loose = tokenize.untokenize((token.type, token.string) for token in tokens)
assert loose == "if x :\n y =1 # note\n"
# A stream that began with ENCODING comes back as bytes
raw = list(tokenize.tokenize(io.BytesIO(b"x = 1\n").readline))
assert tokenize.untokenize(raw) == b"x = 1\n"
Errors arrive in the stream¶
TokenError is raised by the generator when it reaches the fault, so every
token of the lines before it has already been yielded. A consumer that keeps
what it has seen keeps the prefix that tokenized.
import io
import tokenize
seen = []
try:
for token in tokenize.generate_tokens(io.StringIO("total = (1,\n").readline):
seen.append(token.string) # each yielded before the fault is reached
except tokenize.TokenError as error:
message, position = error.args
assert "EOF in multi-line statement" in message
assert seen == ["total", "=", "(", "1", ",", "\n"]
The command line prints what tokenize() yields, one token per line:
# Tokenize a file - O(n), but every token is listed before the first is printed
python -m tokenize script.py
# Name each operator exactly (EQUAL, LPAR) instead of OP
python -m tokenize -e script.py
# From standard input the tokens stream, and a fault prints the ones before it
python -m tokenize < script.py