python程序代码解析_Python源码分析3 – 词法分析器PyTokenizer
Introduction
上次我們分析了Python中執行程序可分為5個步驟:
Tokenizer進行詞法分析,把源程序分解為Token
Parser根據Token創建CST
CST被轉換為AST
AST被編譯為字節碼
執行字節碼
本文將介紹Python程序執行的第一步,也就是詞法分析。詞法分析簡單來說就是把源程序的字符分解組合成Token。比如sum=0可以分解成3個token,'sum', '=', '0'。程序中的whitespace通常只作為分隔符用,最終會被忽略掉,因此沒有出現在token的列表中。不過在Python之中,由于語法規則的關系,Tab/Space需要用來分析程序的縮進,因此Python中對于Whitespace的處理比一般C/C++編譯器的處理會要稍微復雜一些。
在Python中詞法分析的實現在Parser目錄下的tokenizer.h和tokenizer.cpp。Python的其他部分會直接調用tokenizer.h中定義的函數,如下:
extern struct tok_state *PyTokenizer_FromString(const char *);extern struct tok_state *PyTokenizer_FromFile(FILE *, char *, char *);extern void PyTokenizer_Free(struct tok_state *);extern int PyTokenizer_Get(struct tok_state *, char **, char **);
這些函數均以PyTokenizer開頭。這是Python源代碼中的一個約定。雖然Python是用C語言實現的,其實現方式借鑒了很多面對對象的思想。拿詞法分析來說,這四個函數均可以看作PyTokenizer的成員函數。頭兩個函數PyTokenizer_FromXXXX可以看作是構造函數,返回PyTokenizer的instance。PyTokenizer對象內部狀態,也就是成員變量,儲存在tok_state之中。PyTokenizer_Free可以看作是析構函數,負責釋放PyTokenizer,也就是tok_state所占用的內存。PyTokenizer_Get則是PyTokenizer的一個成員函數,負責取得在字符流中下一個Token。這兩個函數均需要傳入tok_state的指針,和C++中需要隱含傳入this指針給成員函數的道理是一致的。可以看到,OO的思想其實是和語言無關的,即使是C這樣的結構化的語言,也可以寫出面對對象的程序。
tok_state
tok_state等價于PyTokenizer這個class本身的狀態,也就是內部的私有成員的集合。部分定義如下:
/* Tokenizer state */struct tok_state {/* Input state; buf <= cur <= inp <= end *//* NB an entire line is held in the buffer */char *buf; /* Input buffer, or NULL; malloc'ed if fp != NULL */char *cur; /* Next character in buffer */char *inp; /* End of data in buffer */char *end; /* End of input buffer if buf != NULL */char *start; /* Start of current token if not NULL */int done; /* E_OK normally, E_EOF at EOF, otherwise error code/* NB If done != E_OK, cur must be == inp!!! */FILE *fp; /* Rest of input; NULL if tokenizing a string */int tabsize; /* Tab spacing */int indent; /* Current indentation index */int indstack[MAXINDENT]; /* Stack of indents */int atbol; /* Nonzero if at begin of new line */int pendin; /* Pending indents (if > 0) or dedents (if < 0) */char *prompt, *nextprompt; /* For interactive prompting */int lineno; /* Current line number */int level; /* () [] {} Parentheses nesting level *//* Used to allow free continuations inside them */};
最重要的是buf, cur, inp, end, start。這些field直接決定了緩沖區的內容:
buf是緩沖區的開始。假如PyTokenizer處于字符串模式,那么buf指向字符串本身,否則,指向文件讀入的緩沖區。
cur指向緩沖區中下一個字符。
inp指向緩沖區中有效數據的結束位置。PyTokenizer是以行為單位進行處理的,每一行的內容存入從buf到inp之間,包括\n。一般情況下 ,PyTokenizer會直接從緩沖區中取下一個字符,一旦到達inp所指向的位置,就會準備取下一行。當PyTokenizer處于不同模式下面,具體的行為會稍有不同。
end是緩沖區的結束,在字符串模式下沒有用到。
start指向當前token的開始位置,如果現在還沒有開始分析token,start為NULL。
PyTokenzer_FromString & PyTokenizer_FromFile
PyTokenizer_FromString & PyTokenizer_FromFile可以說是PyTokenizer的構造函數。從這兩個函數的命名可以看出,PyTokenizer支持兩種模式:字符串和文件。由于標準輸入STDIN也可以看作是文件,因此實際上PyTokenizer支持3種模式:字符串,交互,文件。
PyTokenizer_FromFile的實現和PyTokenizer_FromString的實現大致相同。后者的實現如下:
/* Set up tokenizer for string */struct tok_state *PyTokenizer_FromString(const char *str){struct tok_state *tok = tok_new();if (tok == NULL)return NULL;str = (char *)decode_str(str, tok);if (str == NULL) {PyTokenizer_Free(tok);return NULL;}/* XXX: constify members. */tok->buf = tok->cur = tok->end = tok->inp = (char*)str;return tok;}
直接調用tok_new返回一個tok_state的instance,后面的decode_str負責對str進行解碼,然后賦給tok->buf/cur/end/inp。
PyTokenizer_Get
下面我們來分析一下PyTokenizer_Get函數。該函數的作用是在PyTokenizer所綁定的字符流(可以是字符串也可以是文件)中取出下一個token,比如sum=0剛取到了'sum',那么下一個取到的就是'='。一個返回的token由兩部分參數描述,一個是表示token類型的int,一個是token的具體內容,也就是一個字符串。Python會把不同token分為若干種類型,這些不同的類型定義在include/token.h里面以宏的形式存在,如NAME,NUMBER,STRING,NEWLINE等。舉例來說,'sum'這個token可以表示成(NAME, 'sum')。NAME是類型,表明sum是一個名稱(注意請和字符串區分開)。此時Python并不判定該名稱是關鍵字還是標識符,一律統稱為NAME。而這個NAME的內容是'sum'。PyTokenizer_Get返回的int便是token的類型,而兩個參數char **p_start, char **p_end是輸出參數,指向token在PyTokenizer內部緩沖區中的位置。這里采用返回一個p_start和p_end的意圖是避免構造一份token內容的copy,而是直接給出token在緩沖區中的開始和結束的位置。這樣做顯然是為了提高效率。
PyTokenizer_Get的實現如下,直接調用tok_get函數:
IntPyTokenizer_Get(struct tok_state *tok, char **p_start, char **p_end){int result = tok_get(tok, p_start, p_end);if (tok->decoding_erred) {result = ERRORTOKEN;tok->done = E_DECODE;}return result;}
tok_get負責以下幾件事情:
1. 處理縮進
縮進的處理只在一行開始的時候。如果tok_state::atbol(at beginning of line)非0,說明當前處于一行的開始,否則不做處理。
/* Get indentation level */
if (tok->atbol) {
register int col = 0;
register int altcol = 0;
tok->atbol = 0;
for (;;) {
c = tok_nextc(tok);
if (c == ' ')
col++, altcol++;
else if (c == '\t') {
col = (col/tok->tabsize + 1) * tok->tabsize;
altcol = (altcol/tok->alttabsize + 1)
* tok->alttabsize;
}
else if (c == '\014') /* Control-L (formfeed) */
col = altcol = 0; /* For Emacs users */
else
break;
}
tok_backup(tok, c);
上面的代碼負責計算縮進了多少列。由于tab鍵可能有多種設定,PyTokenizer對tab鍵有兩套處理方案:tok->tabsize保存著"標準"的tab的大小,缺省為8(一般不要修改此值)。Tok->alttabsize保存著另外的tab大小,缺省在tok_new中初始化為1。col和altcol保存著在兩種不同tab設置之下的列數,遇到空格+1,遇到\t則跳到下一個tabstop,直到遇到其他字符為止。
if (c == '#' || c == '\n') {
/* Lines with only whitespace and/or comments
shouldn't affect the indentation and are
not passed to the parser as NEWLINE tokens,
except *totally* empty lines in interactive
mode, which signal the end of a command group. */
if (col == 0 && c == '\n' && tok->prompt != NULL)
blankline = 0; /* Let it through */
else
blankline = 1; /* Ignore completely */
/* We can't jump back right here since we still
may need to skip to the end of a comment */
}
接下來,如果遇到了注釋或者是空行,則不加以處理,直接跳過,這樣做是避免影響縮進。唯一的例外是在交互模式下的完全的空行(只有一個換行符)需要被處理,因為在交互模式下空行意味著一組語句將要結束,而在非交互模式下完全的空行是要被直接忽略掉的。
if (!blankline && tok->level == 0) {
if (col == tok->indstack[tok->indent]) {
//情況1:col=當前縮進,不變
}
else if (col > tok->indstack[tok->indent]) {
//情況2:col>當前縮進,進棧
tok->pendin++;
tok->indstack[++tok->indent] = col;
tok->altindstack[tok->indent] = altcol;
}
else /* col < tok->indstack[tok->indent] */ {
//情況3:col<當前縮進,退棧
while (tok->indent > 0 &&
col < tok->indstack[tok->indent]) {
tok->pendin--;
tok->indent--;
}
}
}
最后,根據col和當前indstack的棧頂(也就是當前縮進的位置),確定是哪一種情況,具體請參看上面的代碼。上面的代碼有所刪減,去掉了一些錯誤處理,加上了一點注釋。需要說明的是PyTokenizer維護兩個棧indstack & altindstack,分別對應col和altcol,保存著縮進的位置,而tok->indent保存著棧頂。
2. 跳過whitespace和注釋
代碼很簡單,在此不做說明。
3. 確定token
反復調用tok_nextc,獲得下一個字符,依據字符內容判定是何種token,然后加以返回。具體的過程比較長,但是logic還是比較簡單的。
下面舉一個處理標識符(變量和關鍵字)的例子
/* Identifier (most frequent token!) */
if (isalpha(c) || c == '_') {
/* Process r"", u"" and ur"" */
switch (c) {
case 'r':
case 'R':
c = tok_nextc(tok);
if (c == '"' || c == '\'')
goto letter_quote;
break;
case 'u':
case 'U':
c = tok_nextc(tok);
if (c == 'r' || c == 'R')
c = tok_nextc(tok);
if (c == '"' || c == '\'')
goto letter_quote;
break;
}
while (isalnum(c) || c == '_') {
c = tok_nextc(tok);
}
tok_backup(tok, c);
*p_start = tok->start;
*p_end = tok->cur;
return NAME;
}
假如當前字符是字母或者是下劃線,則開始當作標示符進行分析,否則,繼續執行下面的語句,處理其他的可能性。不過還有一種可能性,Python中字符串可以是用r或者u開頭,比如r"string", u"string"。r代表raw string,u代表unicode string。一旦遇到了r或者u的情況下,直接跳轉到letter_quote標號處,開始作為字符串進行分析。如果不是r/u,反復拿到下一個字符直到下一個字符不是字母,數字或者下劃線為止。由于最后一次拿到的字符不屬于當前標示符,應該被放到下一次進行分析,因此調用tok_backup把字符c回送到緩沖區中,類似ungetch()。最后,設置好p_start & p_end,返回NAME。這樣,返回的結果表明下一個token是NAME,開始于p_start,結束于p_end。
tok_nextc
tok_nextc負責從緩沖區中取出下一個字符,可以說是整個PyTokenizer的最核心的部分。
/* Get next char, updating state; error code goes into tok->done */
static int
tok_nextc(register struct tok_state *tok)
{
for (;;) {
if (tok->cur != tok->inp) {
// cur沒有移動到inp,直接返回*tok->cur++
return Py_CHARMASK(*tok->cur++); /* Fast path */
}
if (tok->fp == NULL) {
//字符串模式
}
if (tok->prompt != NULL) {
//交互模式
}
else {
//磁盤文件模式
}
}
}
大部分情況,tok_nextc會直接返回*tok->cur++,直到tok->cur移動到達tok->inp。一旦tok->cur==tok->inp,tok_nextc會讀入下一行。根據PyTokenizer處于模式的不同,處理方式會不太一樣:
1. 字符串模式
字符串的處理是最簡單的一種情況,如下:
char *end = strchr(tok->inp, '\n');
if (end != NULL)
end++;
else {
end = strchr(tok->inp, '\0');
if (end == tok->inp) {
tok->done = E_EOF;
return EOF;
}
}
if (tok->start == NULL)
tok->buf = tok->cur;
tok->line_start = tok->cur;
tok->lineno++;
tok->inp = end;
return Py_CHARMASK(*tok->cur++);
嘗試獲得下一行的末尾處作為新的inp,否則,說明下一行結尾處沒有\n換行符(說明這是最后一行)或者當前行就是最后一行。在前者的情況下,inp就是字符串\0的位置,否則,返回EOF。當獲得了下一行之后,返回下一個字符Py_CHARMASK(*tok->cur++)。
2. 交互模式
代碼如下:
char *newtok = PyOS_Readline(stdin, stdout, tok->prompt);
if (tok->nextprompt != NULL)
tok->prompt = tok->nextprompt;
if (newtok == NULL)
tok->done = E_INTR;
else if (*newtok == '\0') {
PyMem_FREE(newtok);
tok->done = E_EOF;
}
#if !defined(PGEN) && defined(Py_USING_UNICODE)
else if (tok_stdin_decode(tok, &newtok) != 0)
PyMem_FREE(newtok);
#endif
else if (tok->start != NULL) {
size_t start = tok->start - tok->buf;
size_t oldlen = tok->cur - tok->buf;
size_t newlen = oldlen + strlen(newtok);
char *buf = tok->buf;
buf = (char *)PyMem_REALLOC(buf, newlen+1);
tok->lineno++;
if (buf == NULL) {
PyMem_FREE(tok->buf);
tok->buf = NULL;
PyMem_FREE(newtok);
tok->done = E_NOMEM;
return EOF;
}
tok->buf = buf;
tok->cur = tok->buf + oldlen;
tok->line_start = tok->cur;
strcpy(tok->buf + oldlen, newtok);
PyMem_FREE(newtok);
tok->inp = tok->buf + newlen;
tok->end = tok->inp + 1;
tok->start = tok->buf + start;
}
首先調用PyOs_Readline,獲得下一行。注意newtok所對應的內存是被malloc出來的,最后需要free。由于在交互模式下,第一句話的prompt是>>>,保存在tok->prompt中。從第二句開始提示符是...,保存在tok->nextprompt中,因此需要設置tok->prompt = tok->nextprompt。最后一個else if (tok->start != NULL)的作用是,一旦當讀入下一行的時候,當前token還沒有結束(一個典型的例子是長字符串"""可以跨越多行),由于buf原來的內容不能丟棄,下一行的內容必須加到buf的末尾,。PyTokenizer的做法是調用realloc改變buf的大小,然后把下一行的內容strcpy到buf的末尾。這樣做雖然效率不高,由于一般情況下此種情況發生并不頻繁,而且是處于交互模式下,因此性能上面沒有問題。
3. 文件模式
文件模式下的處理比上面兩種模式都復雜。主要原因是文件模式下一行可能比BUFSIZE大很多,因此一旦BUFSIZE不夠容納一整行的話,必須反復讀入,realloc緩沖區buf,然后把剛剛讀入的內容append到buf的末尾,直到遇到行結束符為止。如果tok->start != NULL,說明當前正在讀入token之中,同樣的當前的buf不能丟棄,因此讀入的新一行的內容同樣也要append到buf的末尾,否則,新一行的內容直接讀入到buf中。由于代碼比較多,這里就不給出了。
評論
#galahython 發表于2007-01-09 14:21:40 IP: 222.66.63.*樓主,抱歉,我沒仔細閱讀這一篇就想提問題了,因為你是用CPython的C源碼來分析的,C程序看得費勁。
我最近在看龍書,上面有一個用DFA來識別字串的簡單算法,我用Python寫了一下,沒問題,接下去有一個NFA轉DFA的算法,用到了subset-construction來實現epsilon-closure的,也用Python寫出來了,但是生成的DFA不知道如何使用,因為這個DFA的每一個狀態,都對應著NFA的一個狀態集,我不是計算機科班出身的沒有理論基礎,所以到這一步就不理解了,一個狀態對應一個狀態集?這樣的DFA如何用?
不知樓主有空時能否講解一下?
我在網上下載了幾個Python的Parser,其中一個也用到了epsilon的算法來構造DFA,但還沒來得及細看。
現在也就是簡單地一問,先把心中的疑惑提出來,呵呵
#ATField 發表于2007-01-12 22:41:13 IP: 124.78.245.*NFA轉DFA之后,NFA其實就沒有用了,DFA的狀態確實對應NFA的狀態集,不過這一點不影響你對DFA的使用,只是定義了DFA和NFA之間的對應關系,對DFA本身并沒有直接影響。因此直接用DFA匹配的算法就可以了,無需考慮NFA
總結
以上是生活随笔為你收集整理的python程序代码解析_Python源码分析3 – 词法分析器PyTokenizer的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 开发工具:收集12 个顶级 Bug 跟踪
- 下一篇: 怎样运行python_怎样运行pytho
