`
chinamming
  • 浏览: 151491 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

Python源码分析2 - 一个简单的Python程序的执行

 
阅读更多

本文主要通过跟踪一个非常简单的Python程序的执行,简单讨论Python实现的基本框架和结构。

要执行Python程序如下,功能非常简单:从1加到10再打印出来

# test program

sum = 0

for i in range(1, 11):

sum = sum + i

print sum

如果想要在Windows下面用VS 2005调试Python,可以通过下面步骤设置:

  1. 把Startup Project设置成Python,这样就可以直接通过F5来启动Python
  2. 右键单击Python这个Project,选择Properties。在对话框中的Configuration Properties->Debugging下面,把Command Arguments设置为-d test.py。其中test.py就是我们所要调试程序的名字。-d表示打开调试开关,可以看到额外的调试信息。

好了,设置完毕之后可以直接按下F10来单步追踪该程序的执行了。

首先,F10,启动程序,可以看到Python的main函数中没有什么内容,只是简单的调用Py_Main。Py_Main顾名思义自然是主函数了,分几大部分:

  1. 分析命令行和环境变量
  2. 调用Py_Initialize初始化
  3. 根据命令行的内容执行进入不同的执行模式

if (command) {

sts = PyRun_SimpleStringFlags(command, &cf) != 0;

free(command);

} else if (module) {

sts = RunModule(module);

free(module);

}

else {

if (filename == NULL && stdin_is_interactive) {

RunStartupFile(&cf);

}

/* XXX */

sts = PyRun_AnyFileExFlags(

fp,

filename == NULL ? "<stdin>" : filename,

filename != NULL, &cf) != 0;

}

从上面的代码可以很容易看出,一共有3种执行方式:

    1. Command模式,执行单条Python语句。通过-c指定。语句内容存放在command变量中。调用PyRun_SimpleStringFlags来执行。
    2. Module模式,执行整个Module。通过-m指定。调用RunModule来执行。
    3. File模式,执行Python源程序和交互都归在此类。可以看到,如果未指定文件名并且stdin是交互的话,会执行一个PYTHONSTARTUP所指定的源程序。

4. 最后调用Py_Finalize结束

这里我们关注的主要对象自然是PyRun_AnyFileExFlags,其内容如下:

/* Parse input from a file and execute it */

int

PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit,

PyCompilerFlags *flags)

{

if (filename == NULL)

filename = "???";

if (Py_FdIsInteractive(fp, filename)) {

int err = PyRun_InteractiveLoopFlags(fp, filename, flags);

if (closeit)

fclose(fp);

return err;

}

else

return PyRun_SimpleFileExFlags(fp, filename, closeit, flags);

}

首先判断文件是否是交互的,如果是,则调用PyRun_InteractiveLoopFlags(fp, filename, flags),否则调用PyRun_SimpleFlagExFlags。PyRun_InteractiveLoopFlags和PyRun_SimpleFileExFlags基本上区别不大,本质上作的事情都是一样,只不过一个是以语句为单位执行,一个是以程序为单位来执行。所以这里还是以分析PyRun_SimpleFileExFlags为主。

int

PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,

PyCompilerFlags *flags)

{

PyObject *m, *d, *v;

const char *ext;

m = PyImport_AddModule("__main__");

if (m == NULL)

return -1;

d = PyModule_GetDict(m);

if (PyDict_GetItemString(d, "__file__") == NULL) {

PyObject *f = PyString_FromString(filename);

if (f == NULL)

return -1;

if (PyDict_SetItemString(d, "__file__", f) < 0) {

Py_DECREF(f);

return -1;

}

Py_DECREF(f);

}

ext = filename + strlen(filename) - 4;

if (maybe_pyc_file(fp, filename, ext, closeit)) {

/* Try to run a pyc file. First, re-open in binary */

if (closeit)

fclose(fp);

if ((fp = fopen(filename, "rb")) == NULL) {

fprintf(stderr, "python: Can't reopen .pyc file/n");

return -1;

}

/* Turn on optimization if a .pyo file is given */

if (strcmp(ext, ".pyo") == 0)

Py_OptimizeFlag = 1;

v = run_pyc_file(fp, filename, d, d, flags);

} else {

v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,

closeit, flags);

}

if (v == NULL) {

PyErr_Print();

return -1;

}

Py_DECREF(v);

if (Py_FlushLine())

PyErr_Clear();

return 0;

}

可以看到此函数主要的作的事情是:

  1. 创建Module __main__,任何Python脚本都会自动属于__main__
  2. 设置__file__为当前文件名字
  3. 检查是否为.pyc / .pyo文件。Maybe_pyc_file会根据文件的扩展名和文件内容的头两个字节是否为0xf2b3来判断是否是pyc/pyo文件。如果是pyc或者pyo,调用run_pyc_file来直接执行Python的Bytecode。
  4. 否则,调用PyRun_FileExFlags来执行Python的源程序

Run_pyc_file不是本文分析的重点,我们还是先来看一下PyRun_FileExFlags这个函数:

PyObject *

PyRun_FileExFlags(FILE *fp, const char *filename, int start, PyObject *globals,

PyObject *locals, int closeit, PyCompilerFlags *flags)

{

PyObject *ret;

mod_ty mod;

PyArena *arena = PyArena_New();

if (arena == NULL)

return NULL;

mod = PyParser_ASTFromFile(fp, filename, start, 0, 0,

flags, NULL, arena);

if (mod == NULL) {

PyArena_Free(arena);

return NULL;

}

if (closeit)

fclose(fp);

ret = run_mod(mod, filename, globals, locals, flags, arena);

PyArena_Free(arena);

return ret;

}

这个函数非常简单,但是却涵盖了整个Python源程序的运行的过程:

  1. 创建一个Arena对象。此对象是用于内存分配用的,维护分配的原始内存和PyObject对象。
  2. 调用PyParser_ASTFromFile。 此函数会通过词法分析和语法分析得到源程序所对应的AST(Abstract Syntax Tree)。
  3. 调用Run_mod
  4. 释放Arena

PyParser_ASTFromFile的代码如下:

mod_ty

PyParser_ASTFromFile(FILE *fp, const char *filename, int start, char *ps1,

char *ps2, PyCompilerFlags *flags, int *errcode,

PyArena *arena)

{

mod_ty mod;

perrdetail err;

node *n = PyParser_ParseFileFlags(fp, filename, &_PyParser_Grammar,

start, ps1, ps2, &err, PARSER_FLAGS(flags));

if (n) {

mod = PyAST_FromNode(n, flags, filename, arena);

PyNode_Free(n);

return mod;

}

else {

err_input(&err);

if (errcode)

*errcode = err.error;

return NULL;

}

}

首先,PyParser_ParseFileFlags分析文件,生成CST(Concrete Syntax Tree)并将其根结点放到node*之中。Node的结构如下:

typedef struct _node {

shortn_type;

char*n_str;

intn_lineno;

intn_col_offset;

intn_nchildren;

struct _node*n_child;

} node;

n_type代表着语法树的结点的类型,对应着Python-2.5/Grammar/Grammar文件中的文法中的终结符/非终结符,主要在graminit.h和token.h中定义。在后面的文章中会详述。N_str则是对应的字符串的内容。N_lineno / n_col_offset分别代表对应行号和列号。最后的两个n_nchildren和n_child表示此节点共有n_nchildren个子结点。

下面的函数可以用来访问node。大部分的函数都无需解释,除了RCHILD,RCHILD表示从右往左第几个,i给负值。比如RCHILD(n, -1)就是从右往左第一个结点。

/* Node access functions */

#define NCH(n)((n)->n_nchildren)

#define CHILD(n, i)(&(n)->n_child[i])

#define RCHILD(n, i)(CHILD(n, NCH(n) + i))

#define TYPE(n)((n)->n_type)

#define STR(n)((n)->n_str)

文中的Python程序的CST看起来大致是这样的:

图中省略了很多细节,因为是CST的关系,分析过程中有大量冗余信息,主要是每一步通过DFA分析的结点都列在这棵树里面了。由于篇幅的关系就不把整棵树画出来了。

那么CST是怎么生成的呢?在PyParser_ParseFileFlags中:

node *

PyParser_ParseFileFlags(FILE *fp, const char *filename, grammar *g, int start,

char *ps1, char *ps2, perrdetail *err_ret, int flags)

{

struct tok_state *tok;

initerr(err_ret, filename);

if ((tok = PyTokenizer_FromFile(fp, ps1, ps2)) == NULL) {

err_ret->error = E_NOMEM;

return NULL;

}

tok->filename = filename;

if (Py_TabcheckFlag || Py_VerboseFlag) {

tok->altwarning = (filename != NULL);

if (Py_TabcheckFlag >= 2)

tok->alterror++;

}

return parsetok(tok, g, start, err_ret, flags);

}

PyParser_ParserFileFlags首先创建tok_state,也就是词法分析器的对象,之后调用parsetok。Parsetok的代码量稍多,这里就不全部列出来了。主干代码如下:

static node *

parsetok(struct tok_state *tok, grammar *g, int start, perrdetail *err_ret,

int flags)

{

parser_state *ps;

node *n;

int started = 0, handling_import = 0, handling_with = 0;

ps = PyParser_New(g, start);

for (;;) {

type = PyTokenizer_Get(tok, &a, &b);

if ((err_ret->error = PyParser_AddToken(ps, (int)type, str, tok->lineno, col_offset, &(err_ret->expected))) != E_OK) {

if (err_ret->error != E_DONE) {

PyObject_FREE(str);

err_ret->token = type;

}

break;

}

}

if (err_ret->error == E_DONE) {

n = ps->p_tree;

ps->p_tree = NULL;

}

else

n = NULL;

PyParser_Delete(ps);

if (n == NULL) {

// error processing

} else if (tok->encoding != NULL) {

node* r = PyNode_New(encoding_decl);

if (!r) {

err_ret->error = E_NOMEM;

n = NULL;

goto done;

}

r->n_str = tok->encoding;

r->n_nchildren = 1;

r->n_child = n;

tok->encoding = NULL;

n = r;

}

done:

PyTokenizer_Free(tok);

return n;

}

里面最重要的是两个函数调用:

  1. PyTokenizer_Get,是用来进行词法分析的,把源程序分解为Token的序列(比如变量名,运算符,关键字等都属于Token)
  2. PyParser_AddToken,把Token加入到DFA状态机中进行语法分析,根据当前的状态和输入的Token,根据跳转表跳转到不同的状态,并依照这个过程生成CST

由于篇幅有限,这个两个函数的实现细节会在以后的文章详细分析。

在PyParser_ParserFileFlags得到了语法树之后,PyAST_FromNode会将CST转换为AST,存入mod_ty中。

typedef _mod *mod_ty;

struct _mod {

enum _mod_kind kind;

union {

struct {

asdl_seq *body;

} Module;

struct {

asdl_seq *body;

} Interactive;

struct {

expr_ty body;

} Expression;

struct {

asdl_seq *body;

} Suite;

} v;

};

_mod是AST的根结点,代表整个Module。在Python-ast.h中定义着所有AST结点的结构,mod_ty和expr_ty也在其中,后者显然代表着一个表达式。

adsl_seq代表一个变长的指针数组,结构定义如下:

typedef struct {

int size;

void *elements[1];

} asdl_seq;

这个结构稍微有些特殊的是elements在struct中只有一个元素。其实这个struct可以支持任意多个元素,正因为如此,普通的定义方法是不行的。因此这里只定义一个元素,然后在分配计算实际的大小(会比adsl_seq这个结构要大),然后访问元素的时候直接用elements,因为C/C++是不会检查越界的。这种做法在C/C++系统编程中还是比较常见的。

Adsl_seq粗看上去只保存了void *,也就是说具体类型信息丢失了,那么当要遍历整个树的时候是如何做的呢?其实当遍历到某个结点(比如Module/Expression)的时候,便可以确定该结点所支持的子结点是什么类型,然后直接转强制转换就可以了。还有一种情况是在结点中直接记录的是具体的有类型的AST结点,比如expr_ty,就更加容易了。

文中Python代码对应的AST大致如下:

同样的,也省略了一些细节。可以看到,最终的结果要比CST要少很多,比较接近源代码本来的样子。

当PyParser_ASTFromFile执行完毕之后,回到PyRun_FileExFlags,执行下一步,调用run_mod,也就是执行这颗AST所代表的程序。

static PyObject *

run_mod(mod_ty mod, const char *filename, PyObject *globals, PyObject *locals,

PyCompilerFlags *flags, PyArena *arena)

{

PyCodeObject *co;

PyObject *v;

co = PyAST_Compile(mod, filename, flags, arena);

if (co == NULL)

return NULL;

v = PyEval_EvalCode(co, globals, locals);

Py_DECREF(co);

return v;

}

Run_mod非常直接,分两步走:

  1. 编译AST,生成PyCodeObject对象,也就是ByteCode。
  2. 调用PyEval_EvalCode执行PyCodeObject,也就是通过VM直接执行bytecode。如果是执行.pyc/.pyo代码的话,直接从文件中读出信息创建好PyCodeObject对象就可以直接执行了,也是调用这个函数。

执行完PyEval_EvalCode之后可以看到55被打印出来了。

总结一下,Python执行程序的过程总共有以下几步:

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

后面的文章中我会对1~5这5步分别进行详细分析,今天就先写到这里。

分享到:
评论

相关推荐

    python课程设计-企业编码管理系统(源码+可执行程序exe+使用说明)

    python课程设计-企业编码管理系统(源码+可执行程序exe+使用说明) 具体的操作步骤如下: (1)生成6位数字防伪编码。 (2)生成9位系列产品数字防伪编码。 (3)生成25位混合产品序列号。 (4)生成含数据分析功能...

    Python小游戏源码-打地鼠游戏源码

    2. **事件循环**:Python小游戏通常包含一个主循环,用于不断检查用户的输入和游戏状态更新。例如,在打地鼠游戏中,这个循环会检查是否有地鼠出现、是否被点击,以及计分情况。 3. **随机生成地鼠**:为了增加游戏...

    python实战项目源码-Word助手-含可执行文件.zip

    在本项目中,“Python实战项目源码-Word助手-含可执行文件.zip”是一个包含Python编程实战项目的压缩包,主要用于实现Word文档的自动化处理。这个工具可能是为了简化和自动化日常与Word相关的繁琐任务,比如数据导入...

    python-2.7.3源码

    Python 2.7.3 是 Python 2.x 系列的一个稳定版本,它包含了丰富的功能和优化,但请注意,此版本已不再维护,官方推荐使用 Python 3.x 版本进行开发。 源码分析: 1. **源码结构**:解压 Src_Python-2.7.3 文件后,...

    python实战项目源码-商城源码-含可执行文件.zip

    这个压缩包文件“python实战项目源码-商城源码-含可执行文件.zip”显然包含了一个基于Python的商城系统的完整源代码,同时也提供了可执行文件,这意味着你可以直接运行该程序而无需编译或构建。这个项目是Python在...

    python项目源码-开心麻花影视作品分析的程序.rar

    标题中的“python项目源码-开心麻花影视作品分析的程序.rar”表明这是一个基于Python编程语言的项目,其主要目标是对开心麻花的影视作品进行数据解析和分析。这个项目可能涉及了数据抓取、清洗、处理以及可视化等多...

    python实现游戏源码-pygame-games.zip

    Pygame是Python的一个库,它为创建2D游戏提供了丰富的功能和简洁的API。Pygame建立在SDL(Simple DirectMedia Layer)之上,提供了窗口管理器、事件处理、声音播放、图像渲染等基本功能,使得开发者可以轻松地构建...

    python实战项目源码-五子棋-含可执行文件.zip

    在本项目中,我们探讨的是一个使用Python编程语言实现的五子棋游戏。五子棋是一种双人对弈的战略棋类游戏,目标是先在棋盘上形成连续的五个同色棋子,横向、纵向或斜向。这个实战项目不仅展示了Python在游戏开发中的...

    python源码-案例框架-实例-05.zip

    在"python源码-案例框架-实例-05.zip"这个压缩包中,我们可以期待找到第五个案例的Python源代码,这个案例可能涉及一个特定的框架,比如Web开发的Django或Flask,或者数据处理的Pandas。通过查看和分析这些源码,...

    python项目源码-火车票分析助手的程序.rar

    标题中的“python项目源码-火车票分析助手的程序.rar”揭示了这是一个使用Python编程语言编写的项目,目的是实现火车票数据分析和管理的功能。这个项目可能包含一系列的Python脚本和相关文件,如数据处理、可视化和...

    python实战项目源码-火车票分析助手-含可执行文件.zip

    这个Python实战项目是一个名为“火车票分析助手”的应用程序,它结合了自动化、数据分析、网络爬虫等技术,可能还涉及到一些游戏开发相关的元素。通过分析给出的文件名,我们可以推测这个压缩包包含的是完整的源代码...

    python-3.10.4-amd64.exe适用于Windows x86_64系统

    综上所述,`python-3.10.4-amd64.exe`是为Windows 64位系统设计的Python版本,不仅包含了语言本身,还提供了开发和运行Python程序所需的全套工具。安装后,开发者可以利用其强大的功能和丰富的库资源进行各种开发...

    python实战项目源码-学生信息管理系统-含可执行文件.zip

    本项目“Python实战项目源码-学生信息管理系统-含可执行文件”是一个典型的Python应用实例,旨在帮助学习者深入理解Python在实际项目中的应用。 一、系统概述 学生信息管理系统是一个常见的管理信息系统,用于存储...

    Python库 | python_vxi11-0.7-py2.7.egg

    一旦安装完成,开发者就可以在Python程序中导入`vxi11`模块,然后使用它提供的函数和类来连接和控制VXI-11设备。例如: ```python from vxi11 import Instrument # 创建一个VXI-11设备连接 dev = Instrument("tcp:...

    Python源码实例-04 简易时钟.rar

    在本压缩包“Python源码实例-04 简易时钟.rar”中,包含了一个用Python编写的简易时钟程序。这个程序是初学者学习Python编程和时间处理的一个典型示例。通过分析和理解这个代码,我们可以深入探讨Python语言的一些...

    Python代码源码-实操案例-框架案例-今天吃什么小程序.zip

    在本压缩包“Python代码源码-实操案例-框架案例-今天吃什么小程序.zip”中,包含的是一个基于Python编程语言的实际操作案例,主要聚焦于框架应用,具体为一个名为“今天吃什么”的小程序。这个案例旨在帮助学习者...

    python实战项目源码-看图猜成语小程序-含可执行文件.zip

    这个Python实战项目是一个看图猜成语的小程序,包含完整的源代码和可执行文件。通过这个项目,我们可以深入学习和理解Python编程在实际应用中的多个方面,包括自动化、数据分析、网络爬虫以及游戏开发等标签所涵盖的...

    python实战项目源码-今天吃什么小程序-含可执行文件.zip

    该项目是基于Python编程语言开发的一个实用小程序,名为“今天吃什么”。这个小程序的目的是帮助用户解决日常生活中关于餐饮选择的困扰,通过自动化的方式提供多样化的菜品推荐。它将结合数据分析、网络爬虫等技术,...

    python源码-案例框架-实例-17 贺卡.zip

    在本压缩包“python源码-案例框架-实例-17 贺卡.zip”中,我们可以探索一个关于Python编程的实例,它涉及到创建一个贺卡应用程序。这个实例旨在帮助学习者理解如何利用Python的基本语法、面向对象编程(OOP)原则...

    易语言调用Python源码例程

    以下是一个简单的类设计示例: ```易语言 .类 开始 类名 Python调用 .成员 变量 python_interpreter 路径.字符串型(0) .成员 函数 初始化 .局部 变量 python_path 路径.字符串型(0) python_path = “C:\Python3...

Global site tag (gtag.js) - Google Analytics