生成器是个什么鬼?
生成器(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'
哦了,生成器就简单介绍到这里, 下面开始正式剖析,这神奇特性的实现原理.
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 模块名/函数名),女神
的喜好(其他各个字段,比如记录函数的入参个数,使用的堆栈大小), 肯定都会铭记于心.
上图通过工具解析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语言那边函数调用是怎么个玩法类,见下图:
每个帧栈保存了函数调用信息(函数参数,局部变量等),函数调用链就由这么一块堆栈数据维护着,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后,日常操作入下图:
每个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基于单线程实现协程的基石