zoukankan      html  css  js  c++  java
  • 《深度剖析CPython解释器》22. 解密Python中的生成器对象,从字节码的角度分析生成器的底层实现以及执行逻辑

    楔子

    下面我们来聊一聊Python中的生成器,它是我们理解后面协程的基础,生成器的话,估计大部分人在写程序的时候都想不到用。但是一旦用好了,确实能给程序带来性能上的提升,那么我们就来看一看吧。

    生成器

    基本用法

    我们知道,一个函数如果它的内部出现了yield关键字,那么它就不再是普通的函数了,而是一个生成器函数。当我们调用的时候,就会创建一个生成器对象。

    生成器对象一般用于处理循环结构,应用得当的话可以极大优化内存使用率。比如:我们读取一个大文件。

    def read_file(file):
        return open(file, encoding="utf-8").readlines()
    
    
    print(read_file("假装是大文件.txt"))
    # ['人生は一体何だろう
    ', 'たぶん 輝いている同時に
    ', '人を苦しくさせるものだろう']
    

    这个版本的函数,直接将里面的内容全部读取出来了,返回了一个列表。如果文件非常大,那么内存的开销可想而知。

    于是我们可以通过yield关键字,将函数变成一个生成器。

    def read_file(file):
        with open(file, encoding="utf-8") as f:
            for line in f:
                yield line
    
    
    data = read_file("假装是大文件.txt")
    print(data)  # <generator object read_file at 0x0000019B4FA8BAC0>
    
    # 这里返回了一个生成器对象, 我们需要使用for循环遍历
    for line in data:
        # 文件每一行自带换行符, 所以这里print就不用换行符了
        print(line, end="")
    """
    人生は一体何だろう
    たぶん 輝いている同時に
    人を苦しくさせるものだろう
    """
    

    观察生成器的运行行为

    那么生成器是怎么做到的呢?我们继续考察一下。

    def read_file(file):
        with open(file, encoding="utf-8") as f:
            for line in f:
                yield line
    
    
    data = read_file("假装是大文件.txt")
    print(data)  # <generator object read_file at 0x0000019B4FA8BAC0>
    
    # 当我们调用初始化函数(生成器函数)时, 会创建一个生成器
    # 此时生成器就已经被创建了
    # 当我们调用__next__的时候, 生成器会执行
    # 一旦执行到yield的时候就会将生成器暂停, 并将yield后面的值返回
    print(next(data), end="")  # 人生は一体何だろう
    
    # 此时生成器处于暂停状态, 如果我们不驱使它的话, 它是不会前进的
    # 当我们再次执行__next__的时候, 生成器恢复执行, 继续处理并在下一个yield处暂停
    print(next(data), end="")  # たぶん 輝いている同時に
    
    # 生成器会记住自己的执行进度, 每次调用next函数, 它总是处理并生产下一个数据, 完全不需要我们担心
    print(next(data))  # 人を苦しくさせるものだろう
    
    # 一旦next的时候, 就会找下一个yield, 但我们知道此时循环已经结束了
    # 所以没有下一个yield了, 因此程序就会报错了
    try:
        print(next(data))
    except StopIteration as e:
        print("生成器执行完毕")  # 生成器执行完毕
    

    因此生成器和之前我们说的迭代器是类似的,毕竟生成器就是一个特殊的迭代器。

    原谅我写到这里有点懈怠了,有点累了。所以这里后面只对生成器的底层实现进行剖析,至于一些Python层面的东西就不说了。

    任务上下文

    在经典的线程模型中,每个线程都有一个独立的执行流,只能执行一个任务。如果一个程序需要同时处理多个任务,可以借助多线程或者多进程技术。假设一个站点需要同时服务于多个客户端连接,可以为每个连接创建一个独立的线程进行处理。

    但不管是线程还是进程,切换时都会带来巨大的性能开销:用户态和内核态的切换、执行上下文保存和恢复、CPU刷新缓存等等。因此,使用进程或者线程来驱动多个小任务的执行,显然不是一个理想的选择。

    那么,除了进程和线程,还有其它的解决方案吗?显然是有的,答案就是协程。不过在讨论之前,我们先来总结多任务执行体系的关键之处。

    一个程序想要同时处理多个任务,必须提供一种能够记录任务执行进度的机制。在经典线程模型中,这个机制由CPU提供:

    如上图,程序内存空间分为代码、数据、堆以及栈等多个段,CPU 中的 CS 寄存器指向代码段,SS 寄存器指向栈段。当程序任务(线程)执行时,IP 寄存器指向代码段中当前正被执行的指令,BP 寄存器指向当前栈帧,SP 寄存器则指向栈顶。

    有了 IP 寄存器,CPU 可以取出需要执行的下一条指令;有了 BP 寄存器,当函数调用结束时,CPU 可以回到调用者继续执行。因此,CPU 寄存器与内存地址空间一起构成了任务执行上下文,记录着任务执行进度。当任务切换时,操作系统先将 CPU 当前寄存器保存到内存,然后恢复待执行任务的寄存器。

    至此,我们已经受到一些启发:生成器不是可以记住自己的执行进度吗?那么,是不是可以用生成器来实现任务执行流?由于生成器在用户态运行,切换成本比线程或进程小很多,是组织微型任务的理想手段。

    现在,我们用生成器来写一个玩具协程,以此体会协程的运行机制:

    def producer(n):
        for i in range(1, n):
            yield f"生产者生产第{i}包子"
    
    
    def consumer(n):
        for i in range(1, n):
            yield f"消费者消费第{i}包子"
    
    
    p = producer(3)
    c = producer(3)
    

    我们创建一个生产包子的生产者,和一个消费包子的消费者,然后进行初始化。当我们调用next函数的时候,就可以驱动它们执行了。

    print(next(p))  # 生产者生产第1包子
    print(next(c))  # 生产者生产第1包子
    

    当遇到第一个yield语句时,让出执行权,并将yield后面的值返回。但是在实例项目中,一般在遇到网络I/O时,才会让出执行权。

    而且我们看到,我们还扮演着调度器的角色。

    print(next(p))  # 生产者生产第2包子
    print(next(c))  # 生产者生产第2包子
    

    我们再次通过next驱动生成器执行,然后遇到yield之后继续暂停,将yield后面的值返回,并将执行权交给我们。

    print(next(p))  
    """
        print(next(p))  # 生产者生产第2包子
    StopIteration
    """
    

    再次驱动生成器执行,但是发现已经找不到下一个yield了,所以抛出StopIteration异常告诉我们生成器已经将全部的值都生成了。

    以上通过一个小例子,感受一下生成器的运行原理,它可以帮我们更好地理解后面的协程。因为协程的思想和生成器本质是一样的,而且在早期Python还没有提供原生协程的时候,就是通过生成器来模拟的协程。

    字节码解密生成器

    上面我们简单的考察了一下生成器的运行时行为,发现了它神秘的一面。生成器可以通过yield关键字暂停,并且还可以通过next函数重新恢复执行(从上一次暂停的位置),这个特性可以用来实现协程。

    而理解协程,首先要理解生成器。

    生成器的创建

    而理解协程,首先要理解生成器。

    def gen():
        print("生成器开始执行了")
    
        name = "夏色祭"
        print("创建了一个局部变量name")
        yield name 
    
        age = -1
        print("创建了一个局部变量age")
        yield age 
        
        gender = "female"
        print("创建了一个局部变量gender")
        yield gender
    

    我们创建一个简单的生成器函数,当我们调用的时候就会得到一个生成器对象。

    def gen():
        print("生成器开始执行了")
    
        name = "夏色祭"
        print("创建了一个局部变量name")
        yield name
    
        age = -1
        print("创建了一个局部变量age")
        yield age
    
        gender = "female"
        print("创建了一个局部变量gender")
        yield gender
    
    
    # 虽然是生成器函数, 但它也是一个函数
    print(gen)  # <function gen at 0x000001DABAD951F0>
    
    # 但是调用生成器函数并不会立刻执行, 而是会返回一个生成器对象
    g = gen()
    print(g)  # <generator object gen at 0x000001D89E9D7270>
    print(g.__class__)  # <class 'generator'>
    

    关于普通函数和生成器函数,有一个非常生动的栗子。普通函数可以想象成一匹马,只要调用了,那么不把里面的代码执行完毕誓不罢休;而生成器函数则好比一头驴,调用的时候并没有动,只是返回一个生成器对象,然后需要每次拿鞭子抽一下(调用一次next)才往前走一步。

    另外我们可以把生成器看成是可以暂停的函数,其中的yield就类似于return,只不过可以有多个yield。当执行到一个yield时,将值返回、同时暂停在此处,然后当使用next函数驱动时,从暂停的地方继续执行,直到找到下一个yield。如果找不到下一个yield,就会抛出StopIteration异常。

    那么老规矩,肯定要看一下字节码。

      1           0 LOAD_CONST               0 (<code object gen at 0x00000188E73D1450, file "generator", line 1>)
                  2 LOAD_CONST               1 ('gen')
                  4 MAKE_FUNCTION            0
                  6 STORE_NAME               0 (gen)
                  8 LOAD_CONST               2 (None)
                 10 RETURN_VALUE
    
    Disassembly of <code object gen at 0x00000188E73D1450, file "generator", line 1>:
      2           0 LOAD_GLOBAL              0 (print)
                  2 LOAD_CONST               1 ('生成器开始执行了')
                  4 CALL_FUNCTION            1
                  6 POP_TOP
    
      4           8 LOAD_CONST               2 ('夏色祭')
                 10 STORE_FAST               0 (name)
    
      5          12 LOAD_GLOBAL              0 (print)
                 14 LOAD_CONST               3 ('创建了一个局部变量name')
                 16 CALL_FUNCTION            1
                 18 POP_TOP
    
      6          20 LOAD_FAST                0 (name)
                 22 YIELD_VALUE
                 24 POP_TOP
    
      8          26 LOAD_CONST               4 (-1)
                 28 STORE_FAST               1 (age)
    
      9          30 LOAD_GLOBAL              0 (print)
                 32 LOAD_CONST               5 ('创建了一个局部变量age')
                 34 CALL_FUNCTION            1
                 36 POP_TOP
    
     10          38 LOAD_FAST                1 (age)
                 40 YIELD_VALUE
                 42 POP_TOP
    
     12          44 LOAD_CONST               6 ('female')
                 46 STORE_FAST               2 (gender)
    
     13          48 LOAD_GLOBAL              0 (print)
                 50 LOAD_CONST               7 ('创建了一个局部变量gender')
                 52 CALL_FUNCTION            1
                 54 POP_TOP
    
     14          56 LOAD_FAST                2 (gender)
                 58 YIELD_VALUE
                 60 POP_TOP
                 62 LOAD_CONST               0 (None)
                 64 RETURN_VALUE
    

    字节码指令依旧是分为两部分,我们先来看看模块的。很简单,就是创建一个生成器对象,而且我们发现字节码和调用一个普通函数的字节码是一样的。那么Python是如何知道这是一个生成器函数呢?显然秘密就在CALL_FUNCTION里面。

    由于我们已经分析过函数了,所以CALL_FUNCTION下一步会调用什么就不多说了,直接用我们之前的一张图:

    2

    CALL_FUNCTION中,它会调用Objects/call.c中的call_function函数。call_function函数可以根据被调用对象的类型进行区别处理,可分为:类方法、函数对象、普通可调用对象等等。

    在这个例子中,被调用的是函数对象。因此call_function内部会调用位于Objects/call.c中的 _PyFunction_FastCallDict 函数,而它则进一步调用位于 Python/ceval.c 中的 _PyEval_EvalCodeWithName 函数,该函数会为PyFunctionObject创建一个栈帧,然后检查co_flags。如果带有 CO_GENERATORCO_COROUTINECO_ASYNC_GENERATOR,那么便创建生成器返回。

    PyObject *
    _PyEval_EvalCodeWithName(PyObject *_co, PyObject *globals, PyObject *locals,
               PyObject *const *args, Py_ssize_t argcount,
               PyObject *const *kwnames, PyObject *const *kwargs,
               Py_ssize_t kwcount, int kwstep,
               PyObject *const *defs, Py_ssize_t defcount,
               PyObject *kwdefs, PyObject *closure,
               PyObject *name, PyObject *qualname)
    {
        //......
        //......
        if (co->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) {
            PyObject *gen;
            int is_coro = co->co_flags & CO_COROUTINE;
            Py_CLEAR(f->f_back);
            if (is_coro) {
                gen = PyCoro_New(f, name, qualname);
            } else if (co->co_flags & CO_ASYNC_GENERATOR) {
                gen = PyAsyncGen_New(f, name, qualname);
            } else {
                gen = PyGen_NewWithQualName(f, name, qualname);
            }
            if (gen == NULL) {
                return NULL;
            }
    
            _PyObject_GC_TRACK(f);
    		
            //直接返回一个生成器
            return gen;
        }
        //......
        //......
    }
    
    

    其中的 co_flags 是PyCodeObject中的一个域,显然它是在编译时由语法规则确定的。我们之前说它是用来判断参数特征的,但其实它还可以用来判断函数特征。

    //Include/code.h
    #define CO_OPTIMIZED    0x0001
    #define CO_NEWLOCALS    0x0002
    #define CO_VARARGS      0x0004
    #define CO_VARKEYWORDS  0x0008
    #define CO_NESTED       0x0010
    #define CO_GENERATOR    0x0020
    

    我们看到 CO_GENERATOR 的值为0x0020。

    print(gen.__code__.co_flags & 0x0020)  # 32
    

    如果不是生成器函数,那么结果为0;现在结果不为0,说明是生成器函数。

    下面我们可以看看生成器函数的底层定义了,位于 Include/genobject.h 中。

    #define _PyGenObject_HEAD(prefix)                                           
        PyObject_HEAD                                                           
        struct _frame *prefix##_frame;                                          
        char prefix##_running;                                                  
        PyObject *prefix##_code;                                                
        PyObject *prefix##_weakreflist;                                         
        PyObject *prefix##_name;                                                
        PyObject *prefix##_qualname;                                            
        _PyErr_StackItem prefix##_exc_state;
    
    typedef struct {
        _PyGenObject_HEAD(gi)
    } PyGenObject;
    
    //如果整理一下, 那么等价于这样
    typedef struct {
        PyObject_HEAD
        struct _frame *gi_frame; //生成器执行时对应的栈帧对象, 用于保存执行上下文信息
        char gi_running;  //标识生成器是否运行中
        PyObject *gi_code; //PyCodeObject对象
        PyObject *gi_weakreflist; //弱引用相关,不深入讨论
        PyObject *gi_name; //生成器名
        PyObject *gi_qualname; //全限定名
        _PyErr_StackItem *gi_exc_state; //生成器执行状态
    } PyGenObject;
    

    所以我们可以画出一张模型图:

    至此生成器对象的创建过程已经浮出水面,与普通函数对象一样,当生成器函数gen被调用时,Python将为其创建栈帧对象,用于维护函数执行上下文。PyCodeObject对象、全局名字空间、局部名字空间、以及运行时栈都在里面。

    但和普通函数不同的是,gen函数的PyCodeObject对象带有生成器标识,在调用的时候,Python不会立刻执行PyCodeObject对象里面的字节码,栈帧对象也不会被接入到调用链中,所以f_back字段此时是空的。相反,Python创建了一个生成器对象,并将其作为函数结果返回。

    我们可以使用Python来验证在我们得到的结论。

    g = gen()
    # 此时刚创建生成器对象, 还没有开始运行
    print(g.gi_running)  # False
    
    # 栈帧对象
    print(g.gi_frame)  # <frame at 0x0000021B6B0969F0,....
    
    # PyCodeObject对象
    print(g.gi_code)  # <code object gen...
    
    # 当然我们还可以通过如下方式获取
    print(g.gi_frame.f_code is g.gi_code is gen.__code__)  # True
    

    生成器的执行

    当执行g = gen()之后,返回生成器对象并交给变量g指向,这个时候还没有开始执行。

    g = gen()
    print(g.gi_frame.f_lasti)  # -1
    

    栈帧对象 f_lasti 字段记录当前字节码执行进度,-1 表示尚未开始执行。

    在使用Python时我们知道,借助 next 内建函数或者 send 方法可以启动生成器,并驱动它不断执行。这意味着,生成器执行的秘密可以通过这两个函数找到。

    我们先从 next 函数入手,作为内建函数,它定义于 Python/bltinmodule.c 源文件,C 语言函数 builtin_next 是也。builtin_next 函数逻辑非常简单,除了类型检查等一些样板式代码,最关键的是这一行:

    res = (*it->ob_type->tp_iternext)(it);
    

    这行代码表明,next 函数实际上调用了生成器类型对象的 tp_iternext 函数完成工作。

    next(g)等价于g.__next__()、等价于g.__class__.__next__(g)。

    g = gen()
    print(g.__next__())
    """
    生成器开始执行了
    创建了一个局部变量name
    夏色祭
    """
    # 其中g.__next__()的返回值就是yield后面的name
    

    那么底层调用了哪个函数呢?我们说类型对象决定实例对象的行为,实例对象相关操作函数的指针都保存在类型对象中。生成器作为 Python 对象中的一员,当然也遵守这一法则。顺着生成器的类型对象 PyGen_Type ( Objects/genobject.c ),很快就可以找到 gen_iternext 函数。

    另一方面, g.send 也可以启动并驱动生成器的执行,根据 Objects/genobject.c 中的方法定义,它底层调用 _PyGen_Send 函数:

    static PyMethodDef coro_methods[] = {
        {"send",(PyCFunction)_PyGen_Send, METH_O, coro_send_doc},
        {"throw",(PyCFunction)gen_throw, METH_VARARGS, coro_throw_doc},
        {"close",(PyCFunction)gen_close, METH_NOARGS, coro_close_doc},
        {NULL, NULL}        /* Sentinel */
    };
    

    不管 gen_iternext 函数还是 _PyGen_Send 函数,都是直接调用 gen_send_ex 函数完成工作的:

    因为不管是哪一种方式驱动,最终都由 gen_send_ex 函数处理,nextsend 的等价性也源于此。经过千辛万苦,我们终于找到了理解生成器运行机制的关键所在。不过这两者虽然是等价的,但是稍稍还有一点不同。

    def test():
        name = yield "value1"
        print(f"name = {name}")
        yield "value2"
    
    
    t = test()
    t.__next__()
    t.__next__()
    """
    name = None
    """
    
    t = test()
    t.__next__()
    t.send("夏色祭")  # name = 夏色祭
    

    这里简单提一下,具体细节可以自己去了解,比较简单。

    此外,gen_send_ex 函数同样位于 Objects/genobject.c 源文件,函数挺长,但最关键的代码只有两行:

    f->f_back = tstate->frame;
    
    // ...
    
    result = PyEval_EvalFrameEx(f, exc);
    

    首先,第一行代码将生成器栈帧挂到当前调用链(或者说栈帧链)上;然后,第二行代码调用 PyEval_EvalFrameEx 开始在生成器栈帧对象中执行字节码;生成器栈帧对象保存着生成器执行上下文,其中 f_lasti 字段跟踪生成器代码对象的执行进度。

    剩下的逻辑我们显然之前就知道了,PyEval_EvalFrameEx 函数最终调用 _PyEval_EvalFrameDefault 函数执行 frame 对象中 f_code 内部的字节码。这个函数我们在虚拟机部分学习过,对它并不陌生。虽然它体量巨大,超过 3 千行代码,但逻辑却非常直白——内部由无限 for 循环逐条遍历字节码,然后交给内部的一个巨型switch case语句,根据不同的指令执行不同的case分支,每执行完一条字节码就自增 f_lasti 字段,直到字节码全部执行完毕、或者中间出现异常时结束循环。

    生成器的暂停

    我们知道,生成器可以利用 yield 语句,将执行权归还给调用者。因此,生成器暂停执行的秘密就隐藏在 yield 语句中。我们先来看看 yield 语句编译后,生成什么字节码。这里我们就直接分析上面生成器函数内部的字节码:

      2           0 LOAD_GLOBAL              0 (print)
                  2 LOAD_CONST               1 ('生成器开始执行了')
                  4 CALL_FUNCTION            1
                  6 POP_TOP
    
      4           8 LOAD_CONST               2 ('夏色祭')
                 10 STORE_FAST               0 (name)
    
      5          12 LOAD_GLOBAL              0 (print)
                 14 LOAD_CONST               3 ('创建了一个局部变量name')
                 16 CALL_FUNCTION            1
                 18 POP_TOP
    
      6          20 LOAD_FAST                0 (name)
                 22 YIELD_VALUE
                 24 POP_TOP
    
      8          26 LOAD_CONST               4 (-1)
                 28 STORE_FAST               1 (age)
    
      9          30 LOAD_GLOBAL              0 (print)
                 32 LOAD_CONST               5 ('创建了一个局部变量age')
                 34 CALL_FUNCTION            1
                 36 POP_TOP
    
     10          38 LOAD_FAST                1 (age)
                 40 YIELD_VALUE
                 42 POP_TOP
    
     12          44 LOAD_CONST               6 ('female')
                 46 STORE_FAST               2 (gender)
    
     13          48 LOAD_GLOBAL              0 (print)
                 50 LOAD_CONST               7 ('创建了一个局部变量gender')
                 52 CALL_FUNCTION            1
                 54 POP_TOP
    
     14          56 LOAD_FAST                2 (gender)
                 58 YIELD_VALUE
                 60 POP_TOP
                 62 LOAD_CONST               0 (None)
                 64 RETURN_VALUE
    

    我们看到每个 yield 语句编译后,都得到这样 3 条字节码指令:

    LOAD_FAST   
    YIELD_VALUE
    POP_TOP
    

    这里是LOAD_FAST,也可以是LOAD_CONST、LOAD_NAME等等,首先这里的LOAD_FAST是将变量指向的值压入运行时栈,设置在栈顶。另外这里变量就是yield右边的值,然后执行 YIELD_VALUE 指令,显然关键就在此处。

            case TARGET(YIELD_VALUE): {
                retval = POP(); //弹出返回值
    
                if (co->co_flags & CO_ASYNC_GENERATOR) {
                    PyObject *w = _PyAsyncGenValueWrapperNew(retval);
                    Py_DECREF(retval);
                    if (w == NULL) {
                        retval = NULL;
                        goto error;
                    }
                    retval = w;
                }
    
                f->f_stacktop = stack_pointer;
                //直接通过goto语句跳出for循环
                goto exit_yielding;
            }
    

    紧接着,_PyEval_EvalFrameDefault 函数将当前栈帧(也就是生成器的栈帧)从栈帧链中解开。注意到,yield 值被 _PyEval_EvalFrameDefault 函数返回,并最终被 send 方法或 next 函数返回给调用者。

    生成器的恢复

    当我们再次调用 next 函数或者 send 方法时,生成器将恢复执行:

    另外通过 send 方法,可以传入一个值,赋给yield所在语句的等号左边的变量,那么这是如何做到的呢?

    首先我们知道,send 方法被调用后,Python 先把生成器栈帧对象挂到栈帧链中,并最终调用 PyEval_EvalFrameEx 函数逐条执行字节码。在这个过程中,send 发送的数据会被放在生成器栈顶:

    g = gen()
    print(g.__next__())
    """
    生成器开始执行了
    创建了一个局部变量name
    夏色祭
    """
    print(g.gi_frame.f_lasti)  # 22
    print(g.gi_frame.f_locals)  # {'name': '夏色祭'}
    
    
    print(g.send("matsuri"))
    """
    创建了一个局部变量age
    -1
    """
    print(g.gi_frame.f_lasti)  # 40
    print(g.gi_frame.f_locals)  # {'name': '夏色祭', 'age': -1}
    

    这里我们 send 一个字符串"matsuri",当然我们的 yield 语句中没有出现赋值,所以这里的值没有变量接收。因此在YIELD_VALUE指令后面就是POP_TOP了,将栈顶的值直接弹出、丢弃。尽管我们没有尝试,但如果假设我们使用变量接收了会怎么样呢?不用想,显然YIELD_VALUE指令后面的POP_TOP会变成STORE_FAST,从栈顶取出 send 发来的值,并保存在局部变量中。

    所以当出现 next 函数或者 send 方法时,这里会再次将生成器对象的栈帧插入到栈帧链中。

    而且我们看到 f_lasti ,它负责保存执行进度,这个进度显然是指字节码指令的便宜量。一开始是 -1 表示生成器没有执行,后面的 2240 则对应各自的YIELD_VALUE。此外,我们看到随着生成器的执行,f_locals也在不断变化。

    再接着生成器按照逻辑有条不紊的执行,每次遇到 yield 就将后面的值返回,并将生成器所在栈帧从栈帧链中移除,同时将f_back设置为空;当使用next函数或者send方法时,再将生成器所在栈帧插入到栈帧链中,然后f_back指向next(g)或者g.send(value)对应的栈帧。然后next(g)或者g.send(value)继续执行,在找到下一个yield之后,继续返回,然后再将生成器所在栈帧的f_back设置为空、并从栈帧链中移除,至于next(g)或者g.send(value),由于它们已经执行完毕,所在栈帧就被销毁了。然后代码继续执行,如果再遇到next(g)或者g.send(value),那么再重复相同的动作,周而复始,直到生成器执行完毕。

    g = gen()
    print(g.__next__())
    """
    生成器开始执行了
    创建了一个局部变量name
    夏色祭
    """
    print(g.gi_frame.f_lasti)  # 22
    print(g.gi_frame.f_locals)  # {'name': '夏色祭'}
    # 我们说next函数或者send方法执行完毕时, 生成器所在栈帧就会从栈帧链中移除, 同时f_back设置为空
    print(g.gi_frame.f_back)  # None
    # 如果是函数, 那么它的栈帧的f_back属性显然是模块对应的栈帧, 但是对于生成器而言则是None
    # 只有在执行时才会被插入到栈帧链中, 并且f_back就是next函数或者send方法调用时所在的栈帧
    
    print(g.send("matsuri"))
    """
    创建了一个局部变量age
    -1
    """
    print(g.gi_frame.f_lasti)  # 40
    print(g.gi_frame.f_locals)  # {'name': '夏色祭', 'age': -1}
    
    
    print(next(g))
    """
    创建了一个局部变量gender
    -female
    """
    print(g.gi_frame.f_lasti)  # 58
    print(g.gi_frame.f_locals)  # {'name': '夏色祭', 'age': -1, 'gender': 'female'}
    
    
    try:
        next(g)
    except StopIteration:
        print("生成器执行完毕")  # 生成器执行完毕
    

    注意:一旦当生成器执行完毕之后,它的 gi_frame 会被设置为None。

    print(g.gi_frame)  # <frame at 0x000002353BB469F0...
    
    try:
        next(g)
    except StopIteration:
        print("生成器执行完毕")  # 生成器执行完毕
    
    print(g.gi_frame)  # None
    

    生成器小结

    至此,生成器执行、暂停、恢复的全部秘密皆已揭开。流程如下:

    • 生成器函数编译后代码对象带有 CO_GENERATOR 标识
    • 如果函数代码对象带 CO_GENERATOR 标识,被调用时 Python 将创建生成器对象
    • 生成器创建的同时,Python 还创建一个栈帧对象,用于维护代码对象执行上下文
    • 调用 next函数或者send方法 驱动生成器执行,然后Python 将生成器栈帧对象插入栈帧链,f_back设置为调用的next函数或者send方法对应的栈帧, 开始执行字节码, 注意: 此时是在next函数或者send方法对应的栈帧中执行
    • 执行到yield语句时,说明next函数或者send方法执行完毕了; 那么将yield右边的值压入运行时栈栈顶, 并终止字节码执行, 退回到上一级栈帧; 并且还会将生成器所在栈帧的f_back设置为空, 以及设置f_lasti等成员
    • yield值最终作为next函数或者send方法的返回值,被调用者取得
    • 当再次调用next函数或者send方法时,Python会将生成器栈帧重新插入到栈帧链中,f_back设置为调用的next函数或者send方法对应的栈帧,然后继续执行生成器内部的字节码。从什么地方开始执行呢?显然是上一次中断的位置,那么上一次中断的位置Python如何得知?没错,显然是通过f_lasti(字节码偏移量),直接从f_lasti处开始执行
    • 执行时,会从f_lasti、即上一次YIELD_VALUE处开始执行,并且会获得调用者通过send发来的值,然后继续往下执行
    • 代码执行权就这样在调用者和生成器间来回切换,然后一直周而复始,直至生成器执行完毕。执行完毕之后,gi_frame就被设置为None了。

    yield和yield from

    除了yield还有一个yield from,估计有人理解不清它们的作用,这里我们简单的提一句。由于我们现在是分析解释器,所以这些Python层面的语法知识,就简单说一下。另外之所以会提到yield from,主要是为了后面的协程做铺垫。

    def f1():
        yield "夏色祭"
    
    
    def f2():
        yield from "夏色祭"
    
    
    f1 = f1()
    f2 = f2()
    
    print(f1.__next__())  # 夏色祭
    print(f2.__next__())  # 夏
    

    yield后面可以跟可迭代对象和不可迭代对象,而yield from后面必须跟可迭代对象。当执行时,会将yield后面的值作为一个整体迭代出来,而yield from则是迭代"可迭代对象里面的一个值"。我们再举个栗子:

    def foo1():
        yield [1, 2, 3]
    
    
    def foo2():
        yield from [1, 2, 3]
    
    f1 = foo1()
    f2 = foo2()
    
    print(f1.__next__())  # [1, 2, 3]
    print(f2.__next__())  # 1
    print(f2.__next__())  # 2
    print(f2.__next__())  # 3
    
    """
    所以我们发现了:
        yield from [1, 2, 3]
        等价于
        for item in [1, 2, 3]
            yield item
    """
    

    如果要是但看上面的吗?会发现yield from貌似没什么特殊的地方,但是yield from它还可以作为委托生成器。

    委托生成器:负责在调用方和子生成器之间建立一个双向通道。

    def foo1():
        for name in ["夏色祭", "神乐mea", "白上吹雪"]:
            yield name
    
        return "yagoo的偶像梦破灭了"
    
    
    def foo2():
        res = yield from foo1()
        print(res)
    
    
    f2 = foo2()
    # 此时不经过foo2, 我们说调用方和子生成器之间建立了一个双向通道
    print(f2.__next__())  # 夏色祭
    print(f2.__next__())  # 神乐mea
    print(f2.__next__())  # 白上吹雪
    try:
        f2.__next__()
    except StopIteration:
        print("迭代结束了")
    """
    yagoo的偶像梦破灭了
    迭代结束了
    """
    

    这里在执行f2.__next__()的时候并没有经过foo2,因为它建立了一个双向通道,我们是直接找到foo1,同理foo1中yield后面的值也会直接返回给调用方。

    一旦foo1中的代码执行完毕,理论上肯定会抛出StopIteration异常,但是有委托生成器。所以会将返回值交给委托生成器中的res,然后在委托生成器中继续寻找yield或者yield from。但是显然res = yield from foo1()这行代码下面已经没有yield或者yield from了,所以异常会由委托生成器抛出来。

    def foo1():
        for name in ["夏色祭", "神乐mea", "白上吹雪"]:
            yield name
    
        return "yagoo的偶像梦破灭了"
    
    
    def foo2():
        yield (yield from foo1())
    
    
    f2 = foo2()
    print(f2.__next__())  # 夏色祭
    print(f2.__next__())  # 神乐mea
    print(f2.__next__())  # 白上吹雪
    print(f2.__next__())  # yagoo的偶像梦破灭了
    
    # 显然这是最标准的写法
    # 当foo1结束之后, 那么yield from foo1()就是foo1的返回值
    # 因此等价于yield "yagoo的偶像梦破灭了", 在寻找下一个yield的时候, 正好将返回值迭代出来
    

    小结

    这次我们介绍了生成器,分析了它的执行原理,相信你对生成器会有一个更深的认识。另外在项目中,可以多尝试使用生成器。

  • 相关阅读:
    network / ethtool / eno
    java使用秘钥 对字符串进行加密、解密
    windows服务器环境下使用jenkins自动化部署vue前端项目
    ESXi下的常用命令
    使用kubeadm手动安装Kubernetes(附带Dashboard)
    cobbler高可用方案
    Linux服务器端口access改为trunk all
    Corosync fence盘替换
    Linux服务器CPU性能模式
    本地代码上传github失败常见错误
  • 原文地址:https://www.cnblogs.com/traditional/p/13620438.html
Copyright © 2011-2022 走看看