MK - Python Web全栈工程师【最新fx】

92 阅读5分钟

Python Web全栈工程师

download: ukoou.com/resource/1414

从底层理解Python的执行

这样的操作码的;对于 递推式构造列表(List Comprehensions)、生成器表达式(generator expressions)以及其他一些有趣的

Python 特性是怎么编译的;从字节码的层面来看,当异常抛出的时候都发生了什么事情。翻阅 CPython 的代码对于解答这些问题当然是很有帮助的,但我仍然觉得以这样的方式来做的话对于理解字节码的执行和堆栈的变化还是缺少点什么。GDB

是个好选择,但是我懒,而且只想使用一些比较高阶的接口写点 Python 代码来完成这件事。

所以呢,我的目标就是创建一个字节码级别的追踪 API,类似 sys.setrace 所提供的那样,但相对而言会有更好的粒度。这充分锻炼了我编写

Python 实现的 C 代码的编码能力。我们所需要的有如下几项,在这篇文章中所用的 Python 版本为 3.5。

一个新的 Cpython 解释器操作码

一种将操作码注入到 Python 字节码的方法

一些用于处理操作码的 Python 代码

一个新的 Cpython 操作码

新操作码:DEBUG_OP

这个新的操作码 DEBUG_OP 是我第一次尝试写 CPython 实现的 C 代码,我将尽可能的让它保持简单。 我们想要达成的目的是,当我们的操作码被执行的时候我能有一种方式来调用一些

Python 代码。同时,我们也想能够追踪一些与执行上下文有关的数据。我们的操作码会把这些信息当作参数传递给我们的回调函数。通过操作码能辨识出的有用信息如下:

堆栈的内容

执行 DEBUG_OP 的帧对象信息

所以呢,我们的操作码需要做的事情是:

找到回调函数

创建一个包含堆栈内容的列表

调用回调函数,并将包含堆栈内容的列表和当前帧作为参数传递给它

听起来挺简单的,现在开始动手吧!声明:下面所有的解释说明和代码是经过了大量段错误调试之后总结得到的结论。首先要做的是给操作码定义一个名字和相应的值,因此我们需要在

Include/opcode.h中添加代码。

/** My own comments begin by '**' **/

/** From: Includes/opcode.h **/

/* Instruction opcodes for compiled code */

/** We just have to define our opcode with a free value

0 was the first one I found **/

#define DEBUG_OP 0

#define POP_TOP 1

#define ROT_TWO 2

#define ROT_THREE 3

这部分工作就完成了,现在我们去编写操作码真正干活的代码。

实现 DEBUG_OP

在考虑如何实现DEBUG_OP之前我们需要了解的是DEBUG_OP提供的接口将长什么样。

拥有一个可以调用其他代码的新操作码是相当酷眩的,但是究竟它将调用哪些代码捏?这个操作码如何找到回调函数的捏?我选择了一种最简单的方法:在帧的全局区域写死函数名。那么问题就变成了,我该怎么从字典中找到一个固定的 C 字符串?为了回答这个问题我们来看看在 Python 的 main loop 中使用到的和上下文管理相关的标识符__enter__和__exit__。

我们可以看到这两标识符被使用在操作码SETUP_WITH中:

/** From: Python/ceval.c **/

TARGET(SETUP_WITH) {

_Py_IDENTIFIER(exit);

_Py_IDENTIFIER(enter);

PyObject *mgr = TOP();

PyObject *exit = special_lookup(mgr, &PyId___exit__), *enter;

PyObject *res;

现在,看一眼宏_Py_IDENTIFIER的定义

/** From: Include/object.h **/

/********************* String Literals ****************************************/

/* This structure helps managing static strings. The basic usage goes like this:

Instead of doing

   r = PyObject_CallMethod(o, "foo", "args", ...);

do

   _Py_IDENTIFIER(foo);

   ...

   r = _PyObject_CallMethodId(o, &PyId_foo, "args", ...);

PyId_foo is a static variable, either on block level or file level. On first

usage, the string "foo" is interned, and the structures are linked. On interpreter

shutdown, all strings are released (through _PyUnicode_ClearStaticStrings).

Alternatively, _Py_static_string allows to choose the variable name.

_PyUnicode_FromId returns a borrowed reference to the interned string.

PyObject{Get,Set,Has}AttrId are getattr versions using _Py_Identifier*.

*/

typedef struct _Py_Identifier {

struct _Py_Identifier *next;

const char* string;

PyObject *object;

} _Py_Identifier;

#define _Py_static_string_init(value) { 0, value, 0 }

#define _Py_static_string(varname, value) static _Py_Identifier varname = _Py_static_string_init(value)

#define _Py_IDENTIFIER(varname) Py_static_string(PyId##varname, #varname)

嗯,注释部分已经说明得很清楚了。通过一番查找,我们发现了可以用来从字典找固定字符串的函数_PyDict_GetItemId,所以我们操作码的查找部分的代码就是长这样滴。

/** Our callback function will be named op_target **/

PyObject *target = NULL;

_Py_IDENTIFIER(op_target);

target = _PyDict_GetItemId(f->f_globals, &PyId_op_target);

if (target == NULL && _PyErr_OCCURRED()) {

if (!PyErr_ExceptionMatches(PyExc_KeyError))

    goto error;

PyErr_Clear();

DISPATCH();

}

为了方便理解,对这一段代码做一些说明:

f是当前的帧,f->f_globals是它的全局区域

如果我们没有找到op_target,我们将会检查这个异常是不是KeyError

goto error;是一种在 main loop 中抛出异常的方法

PyErr_Clear()抑制了当前异常的抛出,而DISPATCH()触发了下一个操作码的执行

下一步就是收集我们想要的堆栈信息。

/** This code create a list with all the values on the current stack **/

PyObject *value = PyList_New(0);

for (i = 1 ; i <= STACK_LEVEL(); i++) {

tmp = PEEK(i);

if (tmp == NULL) {

    tmp = Py_None;

}

PyList_Append(value, tmp);

}

最后一步就是调用我们的回调函数!我们用call_function来搞定这件事,我们通过研究操作码CALL_FUNCTION的实现来学习怎么使用call_function 。

/** From: Python/ceval.c **/

TARGET(CALL_FUNCTION) {

PyObject **sp, *res;

/** stack_pointer is a local of the main loop.

    It's the pointer to the stacktop of our frame **/

sp = stack_pointer;

res = call_function(&sp, oparg);

/** call_function handles the args it consummed on the stack for us **/

stack_pointer = sp;

PUSH(res);

/** Standard exception handling **/

if (res == NULL)

    goto error;

DISPATCH();

}

有了上面这些信息,我们终于可以捣鼓出一个操作码DEBUG_OP的草稿了:

TARGET(DEBUG_OP) {

PyObject *value = NULL;

PyObject *target = NULL;

PyObject *res = NULL;

PyObject **sp = NULL;

PyObject *tmp;

int i;

_Py_IDENTIFIER(op_target);



target = _PyDict_GetItemId(f->f_globals, &PyId_op_target);

if (target == NULL && _PyErr_OCCURRED()) {

    if (!PyErr_ExceptionMatches(PyExc_KeyError))

        goto error;

    PyErr_Clear();

    DISPATCH();

}

value = PyList_New(0);

Py_INCREF(target);

for (i = 1 ; i <= STACK_LEVEL(); i++) {

    tmp = PEEK(i);

    if (tmp == NULL)

        tmp = Py_None;

    PyList_Append(value, tmp);

}



PUSH(target);

PUSH(value);

Py_INCREF(f);

PUSH(f);

sp = stack_pointer;

res = call_function(&sp, 2);

stack_pointer = sp;

if (res == NULL)

    goto error;

Py_DECREF(res);

DISPATCH();

}

在编写 CPython 实现的 C 代码方面我确实没有什么经验,有可能我漏掉了些细节。如果您有什么建议还请您纠正,我期待您的反馈。

编译它,成了!

一切看起来很顺利,但是当我们尝试去使用我们定义的操作码DEBUG_OP的时候却失败了。自从 2008 年之后,Python

使用预先写好的

goto(你也可以从

这里获取更多的讯息)。故,我们需要更新下 goto jump table,我们在 Python/opcode_targets.h 中做如下修改。

/** From: Python/opcode_targets.h **/

/** Easy change since DEBUG_OP is the opcode number 1 **/

static void *opcode_targets[256] = {

//&&_unknown_opcode,

&&TARGET_DEBUG_OP,

&&TARGET_POP_TOP,

/** ... **/

这就完事了,我们现在就有了一个可以工作的新操作码。唯一的问题就是这货虽然存在,但是没有被人调用过。接下来,我们将DEBUG_OP注入到函数的字节码中。

Python封装底层实现原理详解(通俗易懂)

事实上,Python 封装特性的实现纯属“投机取巧”,之所以类对象无法直接调用以双下划线开头命名的类属性和类方法,是因为其底层实现时,Python 偷偷改变了它们的名称。

前面章节中,我们定义了一个 CLanguage 类,定义如下:

class CLanguage :

def setname(self, name):

    if len(name) < 3:

        raise ValueError('名称长度必须大于3!')

    self.__name = name

def getname(self):

    return self.__name

#为 name 配置 setter 和 getter 方法

name = property(getname, setname)

def setadd(self, add):

    if add.startswith("http://"):

        self.__add = add

    else:

        raise ValueError('地址必须以 http:// 开头')

def getadd(self):

    return self.__add



#为 add 配置 setter 和 getter 方法

add = property(getadd, setadd)

#定义个私有方法

def __display(self):

    print(self.__name,self.__add)

注意,在这个类中,有一个 __display() 方法,由于其是私有方法,且该类没有提供任何调用该方法的“接口”,因此在目前看来,此方法根本无法在类外部调用。也就是说,如下调用 __display() 方法是不可行的:

clang = CLanguage()

#尝试调用私有的 display() 方法

clang.__display()

这会导致如下错误:

Traceback (most recent call last):

File "D:\python3.6\1.py", line 33, in

clang.__display()

AttributeError: 'CLanguage' object has no attribute '__display'

那么,是不是类似 display() 这种的私有方法,真的没有方法调用吗?如果你深入了解 Python 封装机制的底层实现原理,就可以调用它。

事实上,对于以双下划线开头命名的类属性或类方法,Python 在底层实现时,将它们的名称都偷偷改成了 "_类名__属性(方法)名" 的格式。

就以 CLanguage 类中的 __display() 为例,Python 在底层将其方法名偷偷改成了“_CLanguage__display()”。例如,在 CLanguage 类的基础上,执行如下代码:

纯文本复制

clang = CLanguage()

#调用name的setname()方法

clang.name = "新宝库"

#调用add的setadd()方法

clang.add = "www.xinbaoku.com"

#直接调用隐藏的display()方法

clang._CLanguage__display()

请点击输入图片描述(最多18字)