日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

python程序代码解析_Python源码分析3 – 词法分析器PyTokenizer

發布時間:2023/12/10 python 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。