Python源码分析3 – 词法分析器PyTokenizer

2024-01-17 08:32

本文主要是介绍Python源码分析3 – 词法分析器PyTokenizer,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

2006年12月12日 00:20:00

Introduction

上次我们分析了Python中执行程序可分为5个步骤:

  1. Tokenizer进行词法分析,把源程序分解为Token
  2. Parser根据Token创建CST
  3. CST被转换为AST
  4. AST被编译为字节码
  5. 执行字节码

本文将介绍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-
return tok;
}

直接调用tok_new返回一个tok_state的instance,后面的decode_str负责对str进行解码,然后赋给tok-

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函数:

Int
PyTokenizer_Get(struct tok_state *tok, char **p_start, char **p_end)
{
int result = tok_get(tok, p_start, p_end);
if (tok- result = ERRORTOKEN;
tok- }
return result;
}

tok_get负责以下几件事情:

1. 处理缩进

缩进的处理只在一行开始的时候。如果tok_state::atbol(at beginning of line)非0,说明当前处于一行的开始,否则不做处理。

/* Get indentation level */
if (tok-
register int col = 0;
register int altcol = 0;
tok-
for (;;) {
c = tok_nextc(tok);
if (c == ' ')
col++, altcol++;
else if (c == '/t') {
col = (col/tok-
altcol = (altcol/tok-
* tok-
}
else if (c == '/014') /* Control-L (formfeed) */
col = altcol = 0; /* For Emacs users */
else
break;
}
tok_backup(tok, c);

上面的代码负责计算缩进了多少列。由于tab键可能有多种设定,PyTokenizer对tab键有两套处理方案:tok-

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-
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-
if (col == tok-
// 情况1col=当前缩进,不变
}
else if (col < tok-
// 情况2col<当前缩进,进栈
tok-
tok-
tok-
}
else /* col > tok-
// 情况3col>当前缩进,退栈
while (tok- < 0 &&
col > tok-
tok-
tok-
}
}
}

最后,根据col和当前indstack的栈顶(也就是当前缩进的位置),确定是哪一种情况,具体请参看上面的代码。上面的代码有所删减,去掉了一些错误处理,加上了一点注释。需要说明的是PyTokenizer维护两个栈indstack & altindstack,分别对应col和altcol,保存着缩进的位置,而tok-

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-
*p_end = tok-
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-
static int
tok_nextc(register struct tok_state *tok)
{
for (;;) {
if (tok-
// cur没有移动到inp,直接返回*tok-
return Py_CHARMASK(*tok-
}
if (tok-
// 字符串模式
}
if (tok-
// 交互模式
}
else {
// 磁盘文件模式
}
}
}

大部分情况,tok_nextc会直接返回*tok-

1. 字符串模式

字符串的处理是最简单的一种情况,如下:

char *end = strchr(tok-
if (end != NULL)
end++;
else {
end = strchr(tok-
if (end == tok-
tok-
return EOF;
}
}
if (tok-
tok-
tok-
tok-
tok-
return Py_CHARMASK(*tok-

尝试获得下一行的末尾处作为新的inp,否则,说明下一行结尾处没有/n换行符(说明这是最后一行)或者当前行就是最后一行。在前者的情况下,inp就是字符串/0的位置,否则,返回EOF。当获得了下一行之后,返回下一个字符Py_CHARMASK(*tok-

2. 交互模式

代码如下:

char *newtok = PyOS_Readline(stdin, stdout, tok-
if (tok-
tok-
if (newtok == NULL)
tok-
else if (*newtok == '/0') {
PyMem_FREE(newtok);
tok-
}
#if !defined(PGEN) && defined(Py_USING_UNICODE)
else if (tok_stdin_decode(tok, &newtok) != 0)
PyMem_FREE(newtok);
#endif
else if (tok-
size_t start = tok-
size_t oldlen = tok-
size_t newlen = oldlen + strlen(newtok);
char *buf = tok-
buf = (char *)PyMem_REALLOC(buf, newlen+1);
tok-
if (buf == NULL) {
PyMem_FREE(tok-
tok-
PyMem_FREE(newtok);
tok-
return EOF;
}
tok-
tok-
tok-
strcpy(tok-
PyMem_FREE(newtok);
tok-
tok-
tok-
}

首先调用PyOs_Readline,获得下一行。注意newtok所对应的内存是被malloc出来的,最后需要free。由于在交互模式下,第一句话的prompt是<<<,保存在tok-

3. 文件模式

文件模式下的处理比上面两种模式都复杂。主要原因是文件模式下一行可能比BUFSIZE大很多,因此一旦BUFSIZE不够容纳一整行的话,必须反复读入,realloc缓冲区buf,然后把刚刚读入的内容append到buf的末尾,直到遇到行结束符为止。如果tok-

作者: ATField
E-Mail: atfield_zhang@hotmail.com
Blog:
http://blog.csdn.net/atfield



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1439068


这篇关于Python源码分析3 – 词法分析器PyTokenizer的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/615452

相关文章

[职场] 公务员的利弊分析 #知识分享#经验分享#其他

公务员的利弊分析     公务员作为一种稳定的职业选择,一直备受人们的关注。然而,就像任何其他职业一样,公务员职位也有其利与弊。本文将对公务员的利弊进行分析,帮助读者更好地了解这一职业的特点。 利: 1. 稳定的职业:公务员职位通常具有较高的稳定性,一旦进入公务员队伍,往往可以享受到稳定的工作环境和薪资待遇。这对于那些追求稳定的人来说,是一个很大的优势。 2. 薪资福利优厚:公务员的薪资和

Python 字符串占位

在Python中,可以使用字符串的格式化方法来实现字符串的占位。常见的方法有百分号操作符 % 以及 str.format() 方法 百分号操作符 % name = "张三"age = 20message = "我叫%s,今年%d岁。" % (name, age)print(message) # 我叫张三,今年20岁。 str.format() 方法 name = "张三"age

springboot家政服务管理平台 LW +PPT+源码+讲解

3系统的可行性研究及需求分析 3.1可行性研究 3.1.1技术可行性分析 经过大学四年的学习,已经掌握了JAVA、Mysql数据库等方面的编程技巧和方法,对于这些技术该有的软硬件配置也是齐全的,能够满足开发的需要。 本家政服务管理平台采用的是Mysql作为数据库,可以绝对地保证用户数据的安全;可以与Mysql数据库进行无缝连接。 所以,家政服务管理平台在技术上是可以实施的。 3.1

一道经典Python程序样例带你飞速掌握Python的字典和列表

Python中的列表(list)和字典(dict)是两种常用的数据结构,它们在数据组织和存储方面有很大的不同。 列表(List) 列表是Python中的一种有序集合,可以随时添加和删除其中的元素。列表中的元素可以是任何数据类型,包括数字、字符串、其他列表等。列表使用方括号[]表示,元素之间用逗号,分隔。 定义和使用 # 定义一个列表 fruits = ['apple', 'banana

Python应用开发——30天学习Streamlit Python包进行APP的构建(9)

st.area_chart 显示区域图。 这是围绕 st.altair_chart 的语法糖。主要区别在于该命令使用数据自身的列和指数来计算图表的 Altair 规格。因此,在许多 "只需绘制此图 "的情况下,该命令更易于使用,但可定制性较差。 如果 st.area_chart 无法正确猜测数据规格,请尝试使用 st.altair_chart 指定所需的图表。 Function signa

python实现最简单循环神经网络(RNNs)

Recurrent Neural Networks(RNNs) 的模型: 上图中红色部分是输入向量。文本、单词、数据都是输入,在网络里都以向量的形式进行表示。 绿色部分是隐藏向量。是加工处理过程。 蓝色部分是输出向量。 python代码表示如下: rnn = RNN()y = rnn.step(x) # x为输入向量,y为输出向量 RNNs神经网络由神经元组成, python

python 喷泉码

因为要完成毕业设计,毕业设计做的是数据分发与传输的东西。在网络中数据容易丢失,所以我用fountain code做所发送数据包的数据恢复。fountain code属于有限域编码的一部分,有很广泛的应用。 我们日常生活中使用的二维码,就用到foutain code做数据恢复。你遮住二维码的四分之一,用手机的相机也照样能识别。你遮住的四分之一就相当于丢失的数据包。 为了实现并理解foutain

python 点滴学

1 python 里面tuple是无法改变的 tuple = (1,),计算tuple里面只有一个元素,也要加上逗号 2  1 毕业论文改 2 leetcode第一题做出来

高仿精仿愤怒的小鸟android版游戏源码

这是一款很完美的高仿精仿愤怒的小鸟android版游戏源码,大家可以研究一下吧、 为了报复偷走鸟蛋的肥猪们,鸟儿以自己的身体为武器,仿佛炮弹一样去攻击肥猪们的堡垒。游戏是十分卡通的2D画面,看着愤怒的红色小鸟,奋不顾身的往绿色的肥猪的堡垒砸去,那种奇妙的感觉还真是令人感到很欢乐。而游戏的配乐同样充满了欢乐的感觉,轻松的节奏,欢快的风格。 源码下载

高度内卷下,企业如何通过VOC(客户之声)做好竞争分析?

VOC,即客户之声,是一种通过收集和分析客户反馈、需求和期望,来洞察市场趋势和竞争对手动态的方法。在高度内卷的市场环境下,VOC不仅能够帮助企业了解客户的真实需求,还能为企业提供宝贵的竞争情报,助力企业在竞争中占据有利地位。 那么,企业该如何通过VOC(客户之声)做好竞争分析呢?深圳天行健企业管理咨询公司解析如下: 首先,要建立完善的VOC收集机制。这包括通过线上渠道(如社交媒体、官网留言