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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/410412.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

模板 - 快速沃尔什变换

空 转载于:https://www.cnblogs.com/Yinku/p/11253632.html

Liunx下使用Nginx负载均衡

我是centos8得主机 安装命令: yum install nginx 如果为Ubuntu主机 sudo apt-get install nginx 安装完成后&#xff0c;启用并启动Nginx服务&#xff1a; sudo systemctl enable nginxsudo systemctl start nginx 然后我们有2个服务&#xff1a; 一个地址为: http://xx…

[html] HTML5的video在手机端如何实现进来页面时就自动播放?

[html] HTML5的video在手机端如何实现进来页面时就自动播放&#xff1f; 理想情况autoplaytrue。但是现实是&#xff1a;基本所有浏览器都屏蔽了这个属性。 能实现的现在只有微信了&#xff0c;微信有一套自己的规则的内核&#xff0c;可以使用&#xff1a;controlslist"…

用python写helloworld_Python Helloworld程序简单实现

对于一个学习过编程语言的朋友来说&#xff0c;肯定会对Helloworld这一词汇记忆深刻。几乎在学习语言初期&#xff0c;我们都会通过这一简单的小程序来对编程语言有一个深入的了解。那么今天就为大家介绍一下Python Helloworld程序的实现方法。 在开始Python Helloworld程序的创…

RO38 –比较RemObjects SDK 通道

本文汇总了RemObjects SDK提供的通道:最通用的通道1) HTTP 通道:使用www超文本传输协议的灵活,网络无关的通信. 2) Super TCP 通道: 在客户端服务端灵活通用的双向传输. 3) Super HTTP 通道- new in Vinci常用的通道Frequently used channels 4) 以前的TCP通道: 通过TCP实现轻量…

java实现自动登录,并获取数据

为抓取 web的一些隐私数据&#xff0c;需要先登录&#xff0c;然后才能获取这些数据&#xff0c;用程序来实现&#xff0c;就需要实现自动登录&#xff0c;然后将登录信息保存在Cookie中&#xff0c;以便取得数据时&#xff0c;无须再次登录。以网易邮箱为例&#xff1a;所需ja…

如何给微软提反馈建议以及bug

1.相信各位在使用.net core的过程中多多少少遇到了bug和为解决的坑那么我们肿么联系微软反馈问题你 1.找到vs编辑器中的反馈按钮 2.点击进入网址 如果是vs的问题选择visual studio栏目,如果为编译问题或者其他问题请选择对应的栏目 然后写下你的问题和截图&#xff0c;尽量详…

[html] 如何使用H5唤起原生地图APP(百度、高德、腾讯地图等)

[html] 如何使用H5唤起原生地图APP&#xff08;百度、高德、腾讯地图等&#xff09; <iframe src"sinaweibo://qrcode"> 或 <a href"intent://scan/#Intent;schemezxing;packagecom.google.zxing.client.android;end"" >或使用现在的唤起…

PS3支持的显示标准介绍

PS3的视频输出分别有&#xff1a;① HDMI 1.3版本02年推出HDMI 1.0版本以来&#xff0c;HDMI规范已经升级数次:2004年5月HDMI升级到HDMI 1.1版本&#xff0c;接着为了更好的兼容PC系统&#xff0c;HDMI LLC在05年8月23日推出了HDMI的1.2版-----此次修订增加了若干条非常重要的改…

巧用.mdb后缀数据库做后门

我不否认n早前的那个把asp木马写到图片文件中不失为一种好的方法&#xff0c;其实不仅可以写到图片啦 写到mp3文件里写到doc文件里都是可以的啦 copy 文件名/参数 文件名/参数 生成文件名 这样的方法可以很灵活的运用来达到隐藏文件的目的&#xff0c;具体的参数就是/a以…

windwos docker安装k8s一直staring的解决方法

今天遇到了一个很奇怪的问题 docker中启动k8s一直卡在starting 然后就不动了&#xff0c;找了很多解决方法终于得到解决这里把解决流程写在这里 1.配置镜像加速器 首先登录阿里云&#xff0c;阿里云有一个免费的镜像加速服务&#xff0c;可以加快拉取docker容器的速度这里一定…

对未标记为可安全执行的脚本_三、??XSS跨站脚本攻击

跨站脚本攻击(Cross Site Scripting)&#xff0c;缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码&#xff0c;当用户浏览该页之时&#xff0c;嵌入其中Web里面的Script代码会被执行&#xff0c;从而达到恶意攻击用户的目的。理论上&#xff0c;所有可输入的地方没有对输入…

[html] 在主框架下引入的iframe,如果检测这个iframe是否能打开,如果打不开则跳到404页面

[html] 在主框架下引入的iframe&#xff0c;如果检测这个iframe是否能打开&#xff0c;如果打不开则跳到404页面 首先我们知道 iframe 只有 onload 事件&#xff0c;没有 onerror 事件&#xff0c;无论 iframe 能否正常加载都会正常触发 onload 事件。但是由于场景不同&#x…

【Java学习笔记】线程学习笔记

一、资源 http://blog.csdn.net/axman/article/details/431796 这个博客里有Java多线程、线程池的一系列&#xff0c;从基础开始就很清楚&#xff0c;牛人 二、重点 今天读到了基础篇(三)&#xff0c;跳过了后面的方法介绍&#xff0c;以后估计还得补上来的.... 做笔记...... 1…

TortoiseSVN2IDE.pas源代码

unit TortoiseSVN2IDE; {$R icons.res} interface uses ToolsAPI, SysUtils, Windows, Dialogs, Menus, Registry, ShellApi, Classes, Controls, Graphics, ImgList, ExtCtrls, ActnList,Forms;//增加了对Forms单元的引用 const SVN_PROJECT_EXPLORER 0; SVN_LOG 1…

.net 使用阿里云RocketMQ

1.首先我们来讲解一下消息队列的作用 比如说我们的订单系统&#xff0c;再客户订单生成了以后&#xff0c;可能会有 快递系统&#xff0c;通知系统&#xff0c;和打印系统需要用到当前订单的详细内容 所以这个时候常规的操作是在A里面通过代码调用B&#xff0c;C &#xff…

[html] 使用a标签的download属性下载文件会有跨域问题吗?如何解决?

[html] 使用a标签的download属性下载文件会有跨域问题吗&#xff1f;如何解决&#xff1f; 最近刚遇到这个问题&#xff0c;后台返回的图片链接&#xff0c;点击按钮批量下载&#xff0c;a标签的 download 属性只对同源文件有效&#xff0c; 所以我们这里先把图片 url 转为 bl…

tensorflow 目标分割_Tensorflow中的控制流和优化器

控制流只要对tensorflow有一点了解&#xff0c;都应该知道graph是tensorflow最基本的一个结构。Tensorflow的所有计算都是以图作为依据的。图的nodes表示一些基本的数学运算&#xff0c;比如加法&#xff0c;卷积&#xff0c;pool等。Node使用protoBuf来进行描述&#xff0c;包…

Reflector 已经out了,试试ILSpy

Reflector是.NET开发中必备的反编译工具。即使没有用在反编译领域&#xff0c;也常常用它来检查程序集的命名规范&#xff0c;命名空间是否合理&#xff0c;组织类型的方法是否需要改善。举例说明&#xff0c;它有一个可以查看程序集完整名称的功能&#xff0c;请看下图 这里的…

Go语言通过odbc驱动连接华为高斯数据库

1.下载odbc驱动 下载后安装psqlodbc_x64.msi 安装成功后可在odbc数据源中看到以下内容 2.测试odbc驱动 在用户dsn中选择添加 输入对应的连接内容点击test 如果显示以下内容则表示驱动正常可使用odbc连接高斯数据库 3.go 语言编写 其中依赖包需要使用命令安装 go get github.c…