python生成器源码戏说

2,755 阅读13分钟

Python生成器源码剖析

生成器是个什么鬼?

生成器(Generator)在python2.3时成为python的标准特性,因此也多加了一个yield的关键字.(是的,就是java线程让步的那个yield).生成器最神奇的特性就是: 一个函数可以返回多次结果,而不是像普通函数一样只返回一次.(神不神奇,惊不惊喜~)

普通的python函数内部, 加个yield关键字, python解析器就将该函数视为一个生成器函数. 但是生成器函数不是生成器本身,而是生成器工厂.所以调用一个生成器函数时, 将创建一个生成器对象. 当外部需要从这个生成器获取值时,生成器会通过yield返回值,而非普通函数的return方法.这个过程中, yield偷偷做了两件事:

  • 将值返回给调用方
  • 标记当前执行位置, 当生成器再运行时,从标记位置恢复运行

说了这么多,可以上代码了

def return_a_generator():  # 这货是个生成器函数
    yield 'foobar'
    yield 42
    yield 'hello'
generator = return_a_generator()   #这步操作只是为了产生生成器对象, 也可以称为激活
next(generator) # 真二八经的第一次调用,next就是一个调用方
'foobar'
next(generator) # 我还可以被调用哦
42
next(generator) # 这么优秀的我还是可以被调用
'hello'
next(generator)  # 好吧玩脱了
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

<ipython-input-18-8b45440e27eb> in <module>
----> 1 next(generator)  # 好吧玩脱了


StopIteration: 

哦了,生成器就简单介绍到这里, 下面开始正式剖析,这神奇特性的实现原理.

Python运行时核心对象

python世界里,所有东西都是对象,不仅我们看的到基本类型(int, str, list等实例),类本身也是对象哦!但这都不算啥,真正令人叫绝的是,python各种运行时核心组件(代码块, 函数,帧)也都是对象. 下面就依次介绍涉及生成器流程的各个核心对象。(为了使文章不至于太枯燥,将穿插一段狗血虐心的言情剧,大致剧情是女神(一段生成器代码)如何在一个个备胎的助攻下,最终跟渣男(cpu)走在了一起)

PyCodeObject(1号备胎)

当python代码(py文件)被python虚拟机编译后(即将python源码转为python字节码),会将编译结果保存到pyc文件中,pyc文件里 保存的格式就是PyCodeObject的序列化格式.因此他是女神的第一个备胎.PyCodeObject 真容如下:

typedef struct {
    PyObject_HEAD
    int co_argcount;		/* #arguments, except *args */
    int co_nlocals;		/* #local variables */
    int co_stacksize;		/* #entries needed for evaluation stack */
    int co_flags;		/* CO_..., see below */
    PyObject *co_code;		/* instruction opcodes python字节码,女神本尊*/
    PyObject *co_consts;	/* list (constants used) */
    PyObject *co_names;		/* list of strings (names used) */
    PyObject *co_varnames;	/* tuple of strings (local variable names) */
    PyObject *co_freevars;	/* tuple of strings (free variable names) */
    PyObject *co_cellvars;      /* tuple of strings (cell variable names) */
    /* The rest doesn't count for hash/cmp */
    PyObject *co_filename;	/* string (where it was loaded from) 认识女神的地方*/
    PyObject *co_name;		/* string (name, for reference) 女神的名字*/
    int co_firstlineno;		/* first source line number */
    PyObject *co_lnotab;	/* string (encoding addr<->lineno mapping) See
				   Objects/lnotab_notes.txt for details. */
    void *co_zombieframe;     /* for optimization only (see frameobject.c) */
    PyObject *co_weakreflist;   /* to support weakrefs to code objects */
} PyCodeObject;
// python2.7 Include/code.h 

上面就是PyCodeObject c结构体定义了. 其中co_code 字段就是记录女神的本尊(pythn代码对应的python字码)。既然成功追求 到了女神(虽然是短暂的),那第一次相遇的地点(co_filename python源路径),女神的名字(co_name 模块名/函数名),女神 的喜好(其他各个字段,比如记录函数的入参个数,使用的堆栈大小), 肯定都会铭记于心.

PyCodeObject

上图通过工具解析pyc文件的结果,代码就是上文的示例代码,其中co_flags值0x63关注下,是后续的一个关键点.(安利一波该解析器,来源于当初写的乞丐版python虚拟机模拟器工具,!求点赞

OK!一号备胎就介绍完了. 他主要记录了一个python函数的所有静态信息,主要面向存储. 后面为了让其中保存的co_code运行起 来,就要开始往内存发展了.

PyFunctionObject (千斤顶--换胎时才使用,你懂的)

话说女神1号备胎(PyCodeObject)的呵护下,在硬盘里待了很久,但是她始终想去地方是内存和cpu,真正让她充满活力的地方。 终于一次偶然机会她认识了PyFunctionObject,这个专门负责将人引入内存的家伙。于是女神将想法告诉1号备胎,1号备胎听后,为了爱情就将女神让给了PyFunctionObject(但别羡慕,这家伙是这个剧本里面最可怜的存在).

说回人话: PyFunctionObject就是python的函数对象,生成器函数是基于函数改造的,所以python虚拟机从pyc文件加载后,首先变成的就是PyFunctionObjec对象。其结构定义如下:

typedef struct {
    PyObject_HEAD
    PyObject *func_code;	/* A code object 1号备胎保存的所有女神信息 */
    PyObject *func_globals;	/* A dictionary (other mappings won't do) */
    PyObject *func_defaults;	/* NULL or a tuple 函数默认值 */
    PyObject *func_closure;	/* NULL or a tuple of cell objects */
    PyObject *func_doc;		/* The __doc__ attribute, can be anything */
    PyObject *func_name;	/* The __name__ attribute, a string object 女神名必须牢记 */
    PyObject *func_dict;	/* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist;	/* List of weak references */
    PyObject *func_module;	/* The __module__ attribute, can be anything */
} PyFunctionObject;
// python2.7 Include/funcobject.h                                              
                                            

PyFunctionObject除了有保存所有女神信息的(1号备胎那捞过来的)func_code字段,当然还保存了当前这个内存的上下文环境(比如全局变量信息 func_globals),不然都不好意思在女神面前吹嘘自己是混内存的.

但是PyFunctionObject仅限于此,只能常年在内存瞎混,根本就没机会跟cpu(女神的终结目标)有一丝接触的机会。所以注定他跟女神的交往是短暂的(只能做个换胎用的千斤顶),很快PyFrameObject就出现了.

PyFrameObject(2号备胎)

话说PyFrameObject(帧对象)都是一批早年在外留学,在c语言那边学了函数调的原理,海归python后立马cpu下面打工的一群家伙. 所以先简单瞅瞅c语言那边函数调用是怎么个玩法类,见下图:

CCall

每个帧栈保存了函数调用信息(函数参数,局部变量等),函数调用链就由这么一块堆栈数据维护着,PyFrameObject就模拟了这个这个结构,然后在python里呼风唤雨,其结构如下:

typedef struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;	/* 上一个frame,可能为None c那边学过来的精髓,构造调用链 */
    PyCodeObject *f_code;	/* PyCodeObject对象 我们的女神*/
    PyObject *f_builtins;	/* builtin 命名空间  (PyDictObject) */
    PyObject *f_globals;	/* global 命名空间 (PyDictObject) */
    PyObject *f_locals;		/* local 命名空间 (any mapping) */
    PyObject **f_valuestack;	/* 运行时栈底 */
    PyObject **f_stacktop;   /* 运行时栈顶 */
    PyObject *f_trace;		/* Trace function */
    PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
    PyThreadState *f_tstate; /* 当前的线程环境 */
    int f_lasti;		/*  上一条字节码指令在f_code中的偏移位置 */
   
    int f_lineno;		/* Current line number */
    int f_iblock;		/* index in f_blockstack */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];	/* 局部变量(入参也是局部变量) + 内层约束变量 + 自由变量 + 栈  */ 
} PyFrameObject;
// python2.7 Include/frameobject.h  

话说上文中女神偶然发现PyFrameObject才是她来内存的意义,所以立马就给PyFunctionObject发好人卡了,可伶PyFunctionObject女神手还没捂热,但是同样为了爱情,就把女神介绍给了PyFrameObject。PyFrameObject很高兴从PyFunctionObject那边了解到了女神,并正式拍拖.当然女神的所有信息也从PyFunctionObject那边要到了(f_code字段中)

角色回顾

  • PyCodeObject 保存python代码的静态信息
  • PyFunctionObject 函数对象的运行时内存表示
  • PyFrameObject python虚拟机真正的执行对象

Python生成器调用流程 (渣男CPU的日常)

女神在备胎和千斤顶的一步步助攻下,已经非常靠近CPU这个渣男,现在CPU要开始展示真正的技术了

故事里的渣男一般比较极端,我们这个也不例外。这个渣男每天主要的事就是跟女神谈恋爱,而且大部分情况下是:在跟一个女神接触中, 发现了她的闺蜜,于是会搁置当前女神,转而撩其闺蜜, 然后在跟她闺蜜接触中,又了解的闺蜜的闺蜜...(此操作可无限递归下去),那么由这么一个女神引出的一群女神们,我们可以称为"女神簇".当然作为渣男,肯定会通过多个个女神.创建出多个女神簇(即多线程机制)

PS:一直有人吐槽python的GIL锁导致多核利用不起来,没错这是真的。但是python在遇到IO访问时(网络访问,磁盘读取),当前线程会主动释放GIL锁,所以面对IO密集型操作,python多线程还不是太过鸡肋。(当然协程出现后,线程地位就跟尴尬了)

这个渣男(CPU)为了快速物色到满意的女神,所以就从手下PyFrameObject相处的女神寻找了。由于手下PyFrameObject太多,而且为了在多个女神簇之间来回切换,所以专门制定了一个备忘录PyThreadState,格式如下

typedef struct _ts {
    /* See Python/ceval.c for comments explaining most fields */

    struct _ts *next;
    PyInterpreterState *interp; // 进程信息

    struct _frame *frame; // PyFrameObject对象列表,构成调用链
    int recursion_depth;
    /* 'tracing' keeps track of the execution depth when tracing/profiling.
       This is to prevent the actual trace/profile code from being recorded in
       the trace/profile. */
    int tracing;
    int use_tracing;

    Py_tracefunc c_profilefunc;
    Py_tracefunc c_tracefunc;
    PyObject *c_profileobj;
    PyObject *c_traceobj;

	  PyObject *curexc_type;		// 女神交往时的异常信息,确保一个女神谈崩了,不会影响其他人
    PyObject *curexc_value;     // 
    PyObject *curexc_traceback; //

    PyObject *exc_type;     // 当前女神的交往信息,免得多个女神簇回切换后忘了之前聊到哪了
    PyObject *exc_value;
    PyObject *exc_traceback;

    PyObject *dict;  /* Stores per-thread state */

    int tick_counter; 

    int gilstate_counter;

    PyObject *async_exc; /* Asynchronous exception to raise */
    long thread_id; /* Thread id where this tstate was created */

} PyThreadState;

PyThreadState 就是python记录线程信息的数据结构,但是不是属于python对象.里面主要记录当前线程下的帧栈调用链,当前帧的执行情况,说白了就是线程上下文(linux系统线程切换时,主要就是保存各类寄存器,那些寄存器也是保存类似信息).它内部还有PyInterpreterState的引用,这是记录当前进程信息,这里就不展开了.

CPU在引入PyThreadState后,日常操作入下图:

all

每个PyThreadState记录女神簇的恋爱进度,同时看心情切换不同的女神簇。ok,下面就可以看撩妹操作了:

PyEval_EvalFrameEx -- Python虚拟机执行引擎(撩妹场所)

以下代码,已经过极度简化和演义

PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
    PyThreadState *tstate = PyThreadState_GET(); // 拿出备忘录
    tstate->frame = f;  // 将当前PyFrameObject记录到备忘录里
    PyCodeObject *co = f->f_code; // 从PyFrameObject轻松搭上了女神

    // cpu撩妹众多,已经通过强化学习方法,深刻掌握女神在不同表现下,
    // 应该有的的应对方案(比如肚子疼,立马上热水/感冒了,立马上热水等等)
    // 并起名为“状态机”

    first_instr = PyString_AS_STRING(co->co_code);  //女神第一个举动
    next_instr = first_instr + f->f_lasti + 1; //女神第下一个举动

    // 开始交往了
    for (;;) {

    fast_next_opcode:
        opcode = NEXTOP(); // 获取到女神的当前举动
        switch (opcode) { // 根据不同举动,采用不同应对方案
          case NOP:    // 女神啥举动也没有
            goto fast_next_opcode;  // 敌不动我不动,等待下一个举动
                         
          case MAKE_FUNCTION:    // 女神介绍闺蜜
          {
              v = POP(); 
              x = PyFunction_New(v, f->f_globals); // 安排一个PyFunction把闺蜜接到内存,所以所谓的偶然都是安排好的
              PUSH(x);
              break;
          }

          case CALL_FUNCTION:  // 女神说她有点事
          {
              PyObject **sp;
              PCALL(PCALL_ALL);
              sp = stack_pointer;

              x = call_function(&sp, oparg); // 啥也不说了,联系她闺蜜吧
              stack_pointer = sp;
              PUSH(x);
              if (x != NULL)
                  continue;
              break;
          }

          default:   // 女神这个举动之前没见过啊
            fprintf(stderr,
                "XXX lineno: %d, opcode: %d\n",
                PyFrame_GetLineNumber(f),
                opcode);
            PyErr_SetString(PyExc_SystemError, "unknown opcode");
            why = WHY_EXCEPTION;
            break;
        } /* switch */ 
    } /* main loop */

exit_eval_frame:
    Py_LeaveRecursiveCall();
    tstate->frame = f->f_back;

    return retval;
}

通过上面代码,应该就明白CPU勾搭女神的基础操作了(真有这种状态机就好了,可惜现实中女生应该都是混沌的)

ok,下面继续深入了解,cpu是怎么勾搭上女神的闺蜜的。

CPU为了不使当前女神发现他跟她闺蜜有联系,经过两次封装(call_function->fast_function->PyEval_EvalCodeEx python其中一条调用链路)

static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
    PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);  // 从PyFunction获取女神闺蜜的信息
    
    // 此处剧情需要,略过n行代码

    return PyEval_EvalCodeEx(co, globals,
                             (PyObject *)NULL, (*pp_stack)-n, na,
                             (*pp_stack)-2*nk, nk, d, nd,
                             PyFunction_GET_CLOSURE(func));
    
PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
      PyObject **args, int argcount,   // 位置参数
      PyObject **kws, int kwcount,   // 关键字参数
      PyObject **defs, int defcount,   // 默认参数
      PyObject *closure)        // 闭包
{
  PyThreadState *tstate = PyThreadState_GET();

    register PyFrameObject *f;  
    f = PyFrame_New(tstate, co, globals, locals); // 新找的一位PyFrameObject, 将这位闺蜜先安排给他,所谓的偶遇都是cpu幕后操作的结果

    // #define CO_GENERATOR    0x0020 (注意定义哦,我特意从其他地方捞过来的)
    if (co->co_flags & CO_GENERATOR) {
        /* Don't need to keep the reference to f_back, it will be set
         * when the generator is resumed. */
        Py_XDECREF(f->f_back);
        f->f_back = NULL;

        PCALL(PCALL_GENERATOR);

        /* Create a new generator that owns the ready to run frame
         * and return that as the value. */
        return PyGen_New(f);
    }

    retval = PyEval_EvalFrameEx(f,0); // 开始将闺蜜请到之前的撩妹场所,开始新一轮。。。

    return retval;
}

CPU通过fast_function和PyEval_EvalCodeEx两步风骚操作,就将女神的闺蜜经由PyFunctionObject, PyFrameObject搭桥,正式撩到了.以上是cpu对于普通女神的操作流程。但是对于我们的生成器女神(co_flags 0x20置位的女神,我们之前生成的函数flags是0x63,所以0x20是置位的,因此这个小标志,就是区分普通函数和生成器函数的关键),特别青睐,所以有安排了一个 PyGenObject女神管家,时时关注生成器女神在PyFrameObject里的情况,其结构如下:

typedef struct {
    PyObject_HEAD
	/* The gi_ prefix is intended to remind of generator-iterator. */

	/* Note: gi_frame can be NULL if the generator is "finished" */
	struct _frame *gi_frame; // 当前女神所处的PyFrameObject

	/* True if generator is being executed. */
	int gi_running;  
    
	/* The code object backing the generator */
	PyObject *gi_code;

	/* List of weak reference. */
	PyObject *gi_weakreflist;
} PyGenObject;

由于生成器女神的特殊待遇,所以cpu是不敢将生成器女神的信息保存在备忘录里,全有PyGenObject打理。但是一旦当前女神有点事,cpu立马可以通过PyGenObject,找到对应的PyFrameObject,对应操作如下:

static PyObject *
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
{
    PyThreadState *tstate = PyThreadState_GET(); // 拿到备忘录
    PyFrameObject *f = gen->gi_frame; // 先找到当前女神所处的PyFrameObjec
    PyObject *result;

    /* Generators always return to their most recent caller, not
     * necessarily their creator. */
    f->f_back = tstate->frame; // 将当前PyFrameObject记录到备忘录,不然就在不同女神族之间切换,容易忘了

    gen->gi_running = 1;
    result = PyEval_EvalFrameEx(f, exc); // 可以偷偷勾搭生成器女神了
    gen->gi_running = 0;

    /* Don't keep the reference to f_back any longer than necessary.  It
     * may keep a chain of frames alive or it could create a reference
     * cycle. */
    assert(f->f_back == tstate->frame);
    Py_CLEAR(f->f_back);

    return result;
}

如果生成器女神跟cpu聊累了(yield),那cpu就

tstate->frame = f->f_back; 

从备忘录里抹除生成器女神的线索,反正有PyGenObject盯着,但是如果被其他女神发现了,那问题就大发了了。

结尾

说人话了, python生成器实现原理就是,基于一个"游离"的帧对象(PyFrameObject),调用生成器时,将该"游离"帧对象 挂载到当前帧栈上执行,生成器yield返回时,返回当前的值并从帧栈上卸载。在用户层面通过一个生成器对象,提供一批友好的接口 口,封装了内部保存PyFrameObject的事实.仅此而且,而这也是python基于单线程实现协程的基石