python生成器源碼戲說

Python生成器源碼剖析

生成器是個什麼鬼?

生成器(Generator)在python2.3時成爲python的標準特性,所以也多加了一個yield的關鍵字.(是的,就是java線程讓步的那個yield).生成器最神奇的特性就是: 一個函數能夠返回屢次結果,而不是像普通函數同樣只返回一次.(神不神奇,驚不驚喜~)java

普通的python函數內部, 加個yield關鍵字, python解析器就將該函數視爲一個生成器函數. 可是生成器函數不是生成器自己,而是生成器工廠.因此調用一個生成器函數時, 將建立一個生成器對象. 當外部須要從這個生成器獲取值時,生成器會經過yield返回值,而非普通函數的return方法.這個過程當中, yield偷偷作了兩件事:python

  • 將值返回給調用方
  • 標記當前執行位置, 當生成器再運行時,從標記位置恢復運行

說了這麼多,能夠上代碼了linux

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: 
複製代碼

哦了,生成器就簡單介紹到這裏, 下面開始正式剖析,這神奇特性的實現原理.git

Python運行時核心對象

python世界裏,全部東西都是對象,不只咱們看的到基本類型(int, str, list等實例),類自己也是對象哦!但這都不算啥,真正使人叫絕的是,python各類運行時核心組件(代碼塊, 函數,幀)也都是對象. 下面就依次介紹涉及生成器流程的各個核心對象。(爲了使文章不至於太枯燥,將穿插一段狗血虐心的言情劇,大體劇情是女神(一段生成器代碼)如何在一個個備胎的助攻下,最終跟渣男(cpu)走在了一塊兒)github

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(但別羨慕,這傢伙是這個劇本里面最可憐的存在).app

說回人話: 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基於單線程實現協程的基石

相關文章
相關標籤/搜索