阅读更多

1顶
0踩

编程语言

原创新闻 从底层理解Python的执行

2015-06-01 09:54 by 副主编 mengyidan1988 评论(1) 有7178人浏览
【编者按】下面博文将带你创建一个字节码级别的追踪API以追踪Python的一些内部机制,比如类似YIELDVALUE、YIELDFROM操作码的实现,推式构造列表(List Comprehensions)、生成器表达式(generator expressions)以及其他一些有趣Python的编译。

关于译者:赵斌, OneAPM工程师,常年使用 Python/Perl 脚本,从事 DevOP、测试开发相关的开发工作。业余热爱看书,喜欢 MOOC。

以下为译文

最近我在学习 Python 的运行模型。我对 Python 的一些内部机制很是好奇,比如 Python 是怎么实现类似 YIELDVALUE、YIELDFROM 这样的操作码的;对于 递推式构造列表(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 字节码中注入操作码 DEBUG_OP

有很多方式可以在 Python 字节码中注入新的操作码:
  • 使用 peephole optimizer, Quarkslab就是这么干的
  • 在生成字节码的代码中动些手脚
  • 在运行时直接修改函数的字节码(这就是我们将要干的事儿)

为了创造出一个新操作码,有了上面的那一堆 C 代码就够了。现在让我们回到原点,开始理解奇怪甚至神奇的 Python!

我们将要做的事儿有:
  • 得到我们想要追踪函数的 code object
  • 重写字节码来注入DEBUG_OP
  • 将新生成的 code object 替换回去

和code object有关的小贴士

如果你从没听说过 code object,这里有一个简单的 介绍网路上也有一些相关的 文档可供查阅,可以直接Ctrl+F查找 code object

还有一件事情需要注意的是在这篇文章所指的环境中 code object 是不可变的:
Python 3.4.2 (default, Oct  8 2014, 10:45:20)
[GCC 4.9.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = lambda y : 2
>>> x.__code__
<code object <lambda> at 0x7f481fd88390, file "<stdin>", line 1>
>>> x.__code__.co_name
'<lambda>'
>>> x.__code__.co_name = 'truc'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: readonly attribute
>>> x.__code__.co_consts = ('truc',)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: readonly attribute

但是不用担心,我们将会找到方法绕过这个问题的。
使用的工具

为了修改字节码我们需要一些工具:
  • dis模块用来反编译和分析字节码
  • dis.BytecodePython 3.4 新增的一个特性,对于反编译和分析字节码特别有用
  • 一个能够简单修改 code object 的方法

用dis.Bytecode反编译 code bject 能告诉我们一些有关操作码、参数和上下文的信息。
# Python3.4
>>> import dis
>>> f = lambda x: x + 3
>>> for i in dis.Bytecode(f.__code__): print (i)
...
Instruction(opname='LOAD_FAST', opcode=124, arg=0, argval='x', argrepr='x', offset=0, starts_line=1, is_jump_target=False)
Instruction(opname='LOAD_CONST', opcode=100, arg=1, argval=3, argrepr='3', offset=3, starts_line=None, is_jump_target=False)
Instruction(opname='BINARY_ADD', opcode=23, arg=None, argval=None, argrepr='', offset=6, starts_line=None, is_jump_target=False)
Instruction(opname='RETURN_VALUE', opcode=83, arg=None, argval=None, argrepr='', offset=7, starts_line=None, is_jump_target=False)

为了能够修改code object,我定义了一个很小的类用来复制code object,同时能够按我们的需求修改相应的值,然后重新生成一个新的code object。
class MutableCodeObject(object):
    args_name = ("co_argcount", "co_kwonlyargcount", "co_nlocals", "co_stacksize", "co_flags", "co_code",
                  "co_consts", "co_names", "co_varnames", "co_filename", "co_name", "co_firstlineno",
                   "co_lnotab", "co_freevars", "co_cellvars")

    def __init__(self, initial_code):
        self.initial_code = initial_code
        for attr_name in self.args_name:
            attr = getattr(self.initial_code, attr_name)
            if isinstance(attr, tuple):
                attr = list(attr)
            setattr(self, attr_name, attr)

    def get_code(self):
        args = []
        for attr_name in self.args_name:
            attr = getattr(self, attr_name)
            if isinstance(attr, list):
                attr = tuple(attr)
            args.append(attr)
        return self.initial_code.__class__(*args)

这个类用起来很方便,解决了上面提到的code object不可变的问题。
>>> x = lambda y : 2
>>> m = MutableCodeObject(x.__code__)
>>> m
<new_code.MutableCodeObject object at 0x7f3f0ea546a0>
>>> m.co_consts
[None, 2]
>>> m.co_consts[1] = '3'
>>> m.co_name = 'truc'
>>> m.get_code()
<code object truc at 0x7f3f0ea2bc90, file "<stdin>", line 1>

测试我们的新操作码

我们现在拥有了注入DEBUG_OP的所有工具,让我们来验证下我们的实现是否可用。我们将我们的操作码注入到一个最简单的函数中:
from new_code import MutableCodeObject

def op_target(*args):
    print("WOOT")
    print("op_target called with args <{0}>".format(args))

def nop():
    pass

new_nop_code = MutableCodeObject(nop.__code__)
new_nop_code.co_code = b"\x00" + new_nop_code.co_code[0:3] + b"\x00" + new_nop_code.co_code[-1:]
new_nop_code.co_stacksize += 3

nop.__code__ = new_nop_code.get_code()

import dis
dis.dis(nop)
nop()


# Don't forget that ./python is our custom Python implementing DEBUG_OP
hakril@computer ~/python/CPython3.5 % ./python proof.py
  8           0 <0>
              1 LOAD_CONST               0 (None)
              4 <0>
              5 RETURN_VALUE
WOOT
op_target called with args <([], <frame object at 0x7fde9eaebdb0>)>
WOOT
op_target called with args <([None], <frame object at 0x7fde9eaebdb0>)>

看起来它成功了!有一行代码需要说明一下new_nop_code.co_stacksize += 3

co_stacksize 表示 code object 所需要的堆栈的大小
操作码DEBUG_OP往堆栈中增加了三项,所以我们需要为这些增加的项预留些空间
现在我们可以将我们的操作码注入到每一个 Python 函数中了!

重写字节码

正如我们在上面的例子中所看到的那样,重写 Pyhton 的字节码似乎 so easy。为了在每一个操作码之间注入我们的操作码,我们需要获取每一个操作码的偏移量,然后将我们的操作码注入到这些位置上(把我们操作码注入到参数上是有坏处大大滴)。这些偏移量也很容易获取,使用dis.Bytecode ,就像这样。
def add_debug_op_everywhere(code_obj):
    # We get every instruction offset in the code object
    offsets = [instr.offset for instr in dis.Bytecode(code_obj)]
    # And insert a DEBUG_OP at every offset
    return insert_op_debug_list(code_obj, offsets)

def insert_op_debug_list(code, offsets):
    # We insert the DEBUG_OP one by one
    for nb, off in enumerate(sorted(offsets)):
        # Need to ajust the offsets by the number of opcodes already inserted before
        # That's why we sort our offsets!
        code = insert_op_debug(code, off + nb)
    return code

# Last problem: what does insert_op_debug looks like?

基于上面的例子,有人可能会想我们的insert_op_debug会在指定的偏移量增加一个"\x00",这尼玛是个坑啊!我们第一个DEBUG_OP注入的例子中被注入的函数是没有任何的分支的,为了能够实现完美一个函数注入函数insert_op_debug我们需要考虑到存在分支操作码的情况。

Python 的分支一共有两种:
  • 绝对分支:看起来是类似这样子的Instruction_Pointer = argument(instruction)
  • 相对分支:看起来是类似这样子的Instruction_Pointer += argument(instruction)

相对分支总是向前的
我们希望这些分支在我们插入操作码之后仍然能够正常工作,为此我们需要修改一些指令参数。以下是其逻辑流程:
对于每一个在插入偏移量之前的相对分支而言
  • 如果目标地址是严格大于我们的插入偏移量的话,将指令参数增加 1
  • 如果相等,则不需要增加 1 就能够在跳转操作和目标地址之间执行我们的操作码DEBUG_OP
  • 如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离

对于 code object 中的每一个绝对分支而言
  • 如果目标地址是严格大于我们的插入偏移量的话,将指令参数增加 1
  • 如果相等,那么不需要任何修改,理由和相对分支部分是一样的
  • 如果小于,插入我们的操作码的话并不会影响到跳转操作和目标地址之间的距离

下面是实现:
# Helper
def bytecode_to_string(bytecode):
    if bytecode.arg is not None:
        return struct.pack("<Bh", bytecode.opcode, bytecode.arg)
    return struct.pack("<B", bytecode.opcode)

# Dummy class for bytecode_to_string
class DummyInstr:
    def __init__(self, opcode, arg):
        self.opcode = opcode
        self.arg = arg

def insert_op_debug(code, offset):
    opcode_jump_rel = ['FOR_ITER', 'JUMP_FORWARD', 'SETUP_LOOP', 'SETUP_WITH', 'SETUP_EXCEPT', 'SETUP_FINALLY']
    opcode_jump_abs = ['POP_JUMP_IF_TRUE', 'POP_JUMP_IF_FALSE', 'JUMP_ABSOLUTE']
    res_codestring = b""
    inserted = False
    for instr in dis.Bytecode(code):
        if instr.offset == offset:
            res_codestring += b"\x00"
            inserted = True
        if instr.opname in opcode_jump_rel and not inserted: #relative jump are always forward
            if offset < instr.offset + 3 + instr.arg: # inserted beetwen jump and dest: add 1 to dest (3 for size)
                #If equal: jump on DEBUG_OP to get info before exec instr
                res_codestring += bytecode_to_string(DummyInstr(instr.opcode, instr.arg + 1))
                continue
        if instr.opname in opcode_jump_abs:
            if instr.arg > offset:
                res_codestring += bytecode_to_string(DummyInstr(instr.opcode, instr.arg + 1))
                continue
        res_codestring += bytecode_to_string(instr)
    # replace_bytecode just replaces the original code co_code
    return replace_bytecode(code, res_codestring)

让我们看一下效果如何:
>>> def lol(x):
...     for i in range(10):
...         if x == i:
...             break

>>> dis.dis(lol)
101           0 SETUP_LOOP              36 (to 39)
              3 LOAD_GLOBAL              0 (range)
              6 LOAD_CONST               1 (10)
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 GET_ITER
        >>   13 FOR_ITER                22 (to 38)
             16 STORE_FAST               1 (i)

102          19 LOAD_FAST                0 (x)
             22 LOAD_FAST                1 (i)
             25 COMPARE_OP               2 (==)
             28 POP_JUMP_IF_FALSE       13

103          31 BREAK_LOOP
             32 JUMP_ABSOLUTE           13
             35 JUMP_ABSOLUTE           13
        >>   38 POP_BLOCK
        >>   39 LOAD_CONST               0 (None)
             42 RETURN_VALUE
>>> lol.__code__ = transform_code(lol.__code__, add_debug_op_everywhere, add_stacksize=3)


>>> dis.dis(lol)
101           0 <0>
              1 SETUP_LOOP              50 (to 54)
              4 <0>
              5 LOAD_GLOBAL              0 (range)
              8 <0>
              9 LOAD_CONST               1 (10)
             12 <0>
             13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             16 <0>
             17 GET_ITER
        >>   18 <0>

102          19 FOR_ITER                30 (to 52)
             22 <0>
             23 STORE_FAST               1 (i)
             26 <0>
             27 LOAD_FAST                0 (x)
             30 <0>

103          31 LOAD_FAST                1 (i)
             34 <0>
             35 COMPARE_OP               2 (==)
             38 <0>
             39 POP_JUMP_IF_FALSE       18
             42 <0>
             43 BREAK_LOOP
             44 <0>
             45 JUMP_ABSOLUTE           18
             48 <0>
             49 JUMP_ABSOLUTE           18
        >>   52 <0>
             53 POP_BLOCK
        >>   54 <0>
             55 LOAD_CONST               0 (None)
             58 <0>
             59 RETURN_VALUE

# Setup the simplest handler EVER
>>> def op_target(stack, frame):
...     print (stack)

# GO
>>> lol(2)
[]
[]
[<class 'range'>]
[10, <class 'range'>]
[range(0, 10)]
[<range_iterator object at 0x7f1349afab80>]
[0, <range_iterator object at 0x7f1349afab80>]
[<range_iterator object at 0x7f1349afab80>]
[2, <range_iterator object at 0x7f1349afab80>]
[0, 2, <range_iterator object at 0x7f1349afab80>]
[False, <range_iterator object at 0x7f1349afab80>]
[<range_iterator object at 0x7f1349afab80>]
[1, <range_iterator object at 0x7f1349afab80>]
[<range_iterator object at 0x7f1349afab80>]
[2, <range_iterator object at 0x7f1349afab80>]
[1, 2, <range_iterator object at 0x7f1349afab80>]
[False, <range_iterator object at 0x7f1349afab80>]
[<range_iterator object at 0x7f1349afab80>]
[2, <range_iterator object at 0x7f1349afab80>]
[<range_iterator object at 0x7f1349afab80>]
[2, <range_iterator object at 0x7f1349afab80>]
[2, 2, <range_iterator object at 0x7f1349afab80>]
[True, <range_iterator object at 0x7f1349afab80>]
[<range_iterator object at 0x7f1349afab80>]
[]
[None]

甚好!现在我们知道了如何获取堆栈信息和 Python 中每一个操作对应的帧信息。上面结果所展示的结果目前而言并不是很实用。在最后一部分中让我们对注入做进一步的封装。
增加Python封装

正如您所见到的,所有的底层接口都是好用的。我们最后要做的一件事是让 op_target 更加方便使用(这部分相对而言比较空泛一些,毕竟在我看来这不是整个项目中最有趣的部分)。

首先我们来看一下帧的参数所能提供的信息,如下所示:
  • f_code当前帧将执行的 code object
  • f_lasti当前的操作(code object 中的字节码字符串的索引)

经过我们的处理我们可以得知DEBUG_OP之后要被执行的操作码,这对我们聚合数据并展示是相当有用的。

新建一个用于追踪函数内部机制的类:
  • 改变函数自身的co_code
  • 设置回调函数作为op_debug的目标函数

一旦我们知道下一个操作,我们就可以分析它并修改它的参数。举例来说我们可以增加一个auto-follow-called-functions的特性。
def op_target(l, f, exc=None):
    if op_target.callback is not None:
        op_target.callback(l, f, exc)

class Trace:
    def __init__(self, func):
        self.func = func

    def call(self, *args, **kwargs):
        self.add_func_to_trace(self.func)
        # Activate Trace callback for the func call
        op_target.callback = self.callback
        try:
            res = self.func(*args, **kwargs)
        except Exception as e:
            res = e
        op_target.callback = None
        return res

    def add_func_to_trace(self, f):
        # Is it code? is it already transformed?
        if not hasattr(f ,"op_debug") and hasattr(f, "__code__"):
            f.__code__ = transform_code(f.__code__, transform=add_everywhere, add_stacksize=ADD_STACK)
            f.__globals__['op_target'] = op_target
            f.op_debug = True

    def do_auto_follow(self, stack, frame):
        # Nothing fancy: FrameAnalyser is just the wrapper that gives the next executed instruction
        next_instr = FrameAnalyser(frame).next_instr()
        if "CALL" in next_instr.opname:
            arg = next_instr.arg
            f_index = (arg & 0xff) + (2 * (arg >> 8))
            called_func = stack[f_index]

            # If call target is not traced yet: do it
            if not hasattr(called_func, "op_debug"):
                self.add_func_to_trace(called_func)

现在我们实现一个 Trace 的子类,在这个子类中增加 callback 和 doreport 这两个方法。callback 方法将在每一个操作之后被调用。doreport 方法将我们收集到的信息打印出来。

这是一个伪函数追踪器实现:
class DummyTrace(Trace):
    def __init__(self, func):
        self.func = func
        self.data = collections.OrderedDict()
        self.last_frame = None
        self.known_frame = []
        self.report = []

    def callback(self, stack, frame, exc):
        if frame not in self.known_frame:
            self.known_frame.append(frame)
            self.report.append(" === Entering New Frame {0} ({1}) ===".format(frame.f_code.co_name, id(frame)))
            self.last_frame = frame
        if frame != self.last_frame:
            self.report.append(" === Returning to Frame {0} {1}===".format(frame.f_code.co_name, id(frame)))
            self.last_frame = frame

        self.report.append(str(stack))
        instr = FrameAnalyser(frame).next_instr()
        offset = str(instr.offset).rjust(8)
        opname = str(instr.opname).ljust(20)
        arg = str(instr.arg).ljust(10)
        self.report.append("{0}  {1} {2} {3}".format(offset, opname, arg, instr.argval))
        self.do_auto_follow(stack, frame)

    def do_report(self):
        print("\n".join(self.report))

这里有一些实现的例子和使用方法。格式有些不方便观看,毕竟我并不擅长于搞这种对用户友好的报告的事儿。
  • 例1自动追踪堆栈信息和已经执行的指令
  • 例2上下文管理

递推式构造列表(List Comprehensions)的追踪示例 。
  • 例3伪追踪器的输出
  • 例4输出收集的堆栈信息

总结

这个小项目是一个了解 Python 底层的良好途径,包括解释器的 main loop,Python 实现的 C 代码编程、Python 字节码。通过这个小工具我们可以看到 Python 一些有趣构造函数的字节码行为,例如生成器、上下文管理和递推式构造列表。

这里是这个小项目的完整代码。更进一步的,我们还可以做的是修改我们所追踪的函数的堆栈。我虽然不确定这个是否有用,但是可以肯定是这一过程是相当有趣的。

原文链接:Understanding Python execution from inside: A Python assembly tracer
1
0
评论 共 1 条 请登录后发表评论
1 楼 mangguo 2015-06-01 13:45
最近正好在学 Python课程

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 使用Druid SQL Parser解析SQL

    在以前的博文《使用Spring Boot JPA Specification实现使用JSON数据来查询实体数据》中讲到了目前业务上的需求就是以前老系统是通过配置SQL去抽取一些业务数据的,但现在新系统想通过页面的一些配置化实现跟配置SQL...

  • 不能在查询运算符(Contains 运算符除外)的 LINQ to SQL 实现中使用本地序列。

    很多时候我们在使用linq 查询数据的时候,常常会对数据库和内存进行操作,如果语句逻辑衔接过于紧密,稍不留神,在运行的时候就会提示这个错误。 如下图: 此时StockInQuery就是一个内存对象,如果这个查询没有To...

  • SQL 解析原理和使用场景

    - EvalVisitor 用来对SQL表达式求值 - ExportParameterVisitor用来提取SQL中的变量参数 - SchemaStatVisitor 用来统计SQL中使用的表、字段、过滤条件、排序表达式、分组表达式 SQL解析使用场景 整个 SQL 解析的...

  • SQL Server高级查询之子查询(多行子查询)

    多条子查询返回的结果可以是一条数据,因而单行子查询也是多行子查询的一种特殊情况,所以单行子查询的“=”比较符可以替换为多行子查询的“in”比较符。但不能将多行子查询的“in”比较符替换为单行子查询的“=”...

  • java sql 参数化查询_参数化查询为什么能够防止SQL注入

    多数人知道首先:我们要了解SQL收到一个指令后所做的事情:在这里,简单的表示为: 收到指令 -&gt; 编译SQL生成执行计划 -&gt;选择执行计划 -&gt;执行执行计划。具体可能有点不一样,但大致的步骤如上所示。接着我们...

  • 深入理解SQL表连接(join)

    关系数据库中最重要的两个概念,当属表连接和聚合。表连接将一条数据分开成多条,表聚合将多条合成一条。这一分一合,形成了关系数据库强大的逻辑表达能力,这篇文章讲表连接,关于聚合请移步: ...

  • sql中自连接的使用--经典

    sql中自连接的使用 一、用SQL自连接查询处理列之间的关系 SQL自身连接,可以解决很多问题。下面举的一个例子,就是使用了SQL自身连接,它解决了列与列之间的逻辑关系问题,准确的讲是列与列之间的层次关系。SQL...

  • mysql中自连怎么运用_sql中自连接的使用

    一、用SQL自连接查询处理列之间的关系SQL自身连接,可以解决很多问题。下面举的一个例子,就是使用了SQL自身连接,它解决了列与列之间的逻辑关系问题,准确的讲是列与列之间的层次关系。SQL代码如下:1 SELECTFIRST....

  • SQL Server高级查询之T-SQL编程(流程控制语句)

    1.begin…end语句 1.T-SQL的流程控制关键字包括:begin…end、if…else、while、case...1.SQL Sever中的if…else语句与C#和Java语言一样的语意,语法略有不同 3.while循环语句 1.while循环语句是当满足条件就进行循

  • Flask-SQLAlchemy 中多表链接查询(不使用外键)

    SQLAlchemy是一个功能强大的ORM。...本篇介绍我在使用 Flask-SQLAlchemy 2.1 时进行联表查询的一些经验。 表定义 这里有两个表,account 表保存帐号 ID 和昵称,bind 表保存 account 之间的绑定关系。 ...

  • 关系代数和SQL语法

    OLAP计算引擎是一架机器,而操作这架机器的是编程语言。使用者通过特定语言告诉计算引擎,需要读取哪些数据、以及需要进行什么样的计算。

  • Flink SQL 在字节跳动的优化与实践

    简介:Flink 在字节的应用实战整理 | Aven (Flink 社区志愿者)摘要:本文由 Apache Flink Committer,字节跳动架构研发工程师李本超...在 Flink 1.9 发布之前字节跳动内部基于 master 分支进行内部的 SQL 平台构建。经

  • MYSQL语句SQL查询操作

    1 基本SQL查询语句 语法: select 属性列表 from 表名(视图)列表 [where 条件表达式1] [group by 属性名1 [having 条件表达式2]] [order by 属性名2 [asc | desc]] asc:升序 desc:降序 例子: 条件+排序...

  • SQL 注入漏洞介绍及解决办法

    在设计不良的程序当中,忽略了对输入字符串中夹带的 SQL 指令的检查,那么这些夹带进去的指令就会被数据库 误认为是正常的 SQL 指令而运行,从而使数据库受到攻击,可能导致数据被窃取、更改、删除,以及进一步导致...

  • sql中自联结的使用

    sql中自联结的使用一、用SQL自联结查询处理列之间的关系SQL自身连接,可以解决很多问题。下面举的一个例子,就是使用了SQL自身连接,它解决了列与列之间的逻辑关系问题,准确的讲是列与列之间的层次关系。SQL代码...

  • sql in 条件超过 1000 怎么办?

    当SQL中的条件数量超过1000时,有几种方法可以处理: 1. 使用IN和多个值 - 将条件分成多个小组,并将每个小组作为IN子句中的多个值,以...3. 使用子查询 - 将条件值存储在子查询中,然后将该子查询与查询表进行JOIN。

  • 关于sql中自连接

    作为记录 : company 表 id name sex 1 张三 18 2 李四 17 3 王六 20 4 .. .. car 表 id cid car_type car_rmb 1 2 大车 50 2 2 小车 20 3 1 大车 40 4 1 小车 10 5 3 大车 20 6

  • Python项目-自动办公-59 PPT_pptx_在PPT中写入图片和表格.zip

    Python课程设计,含有代码注释,新手也可看懂。毕业设计、期末大作业、课程设计、高分必看,下载下来,简单部署,就可以使用。 包含:项目源码、数据库脚本、软件工具等,该项目可以作为毕设、课程设计使用,前后端代码都在里面。 该系统功能完善、界面美观、操作简单、功能齐全、管理便捷,具有很高的实际应用价值。

  • Python项目-实例-20 快递查询.zip

    Python课程设计,含有代码注释,新手也可看懂。毕业设计、期末大作业、课程设计、高分必看,下载下来,简单部署,就可以使用。 包含:项目源码、数据库脚本、软件工具等,该项目可以作为毕设、课程设计使用,前后端代码都在里面。 该系统功能完善、界面美观、操作简单、功能齐全、管理便捷,具有很高的实际应用价值。

  • 杂货产品检测43-YOLO(v5至v9)、CreateML、Paligemma、TFRecord、VOC数据集合集.rar

    杂货产品检测43-YOLO(v5至v9)、CreateML、Paligemma、TFRecord、VOC数据集合集.rarIPCV分配-V6 2024-01-21 6:10 PM ============================= *与您的团队在计算机视觉项目上合作 *收集和组织图像 *了解和搜索非结构化图像数据 *注释,创建数据集 *导出,训练和部署计算机视觉模型 *使用主动学习随着时间的推移改善数据集 对于最先进的计算机视觉培训笔记本,您可以与此数据集一起使用 该数据集包括7012张图像。 家庭废物以createMl格式注释。 将以下预处理应用于每个图像: *像素数据的自动取向(带有Exif-Arientation剥离) *调整大小为640x640(拉伸) 没有应用图像增强技术。

Global site tag (gtag.js) - Google Analytics