zoukankan      html  css  js  c++  java
  • python中的生成器函数是如何工作的?

    以下内容基于python3.4

    1. python中的普通函数是怎么运行的?

    当一个python函数在执行时,它会在相应的python栈帧上运行,栈帧表示程序运行时函数调用栈中的某一帧。想要获得某个函数相关的栈帧,则必须在调用这个函数且这个函数尚未返回时获取,可能通过inspect模块的currentframe()函数获取当前栈帧。

    栈帧对象中的3个常用的属性:

      • f_back : 调用栈的上一级栈帧
      • f_code: 栈帧对应的c
      • f_locals: 用在当前栈帧时的局部变量;

    比如:

    >>> import inspect
    >>> def func():
    ...   global x
    ...   x = inspect.currentframe()
    ... 
    >>> x = None
    >>> func()
    >>> x
    <frame object at 0x7f50f3ee2868>

    更进一步讲, 标准的python解释器是用C语言写的,通常称作CPython,  当执行一个python函数时,解释器中的C函数 PyEval_EvalFrameEx() 就会被调用,它来处理python 代码的字节码, 它的参数为对于python函数的栈帧 object,即上面例子中的 x就是一个栈帧对象。

    举例说明函数是如何运行的?

    >>> def foo():
    ...   x = 12
    ...   y = bar()
    ...   return y
    ... 
    >>> def bar():
    ...   return 'hello'
    ...

    使用dis模块查看一下函数foo()的字节码(看不懂内容没事,其它有规律):

    >>> import dis
    >>> dis.dis(foo)
      2           0 LOAD_CONST               1 (12)
                  3 STORE_FAST               0 (x)
    
      3           6 LOAD_GLOBAL              0 (bar)
                  9 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
                 12 STORE_FAST               1 (y)
    
      4          15 LOAD_FAST                1 (y)
                 18 RETURN_VALUE

    运行过程:

    解释器调用 C函数 PyEval_EvalFrameEx()运行foo()的字节码,它的参数为foo()对应的栈帧对象,运行位置为foo()对应的栈帧; 在运行过程中,遇到 CALL_FUNCTION 时,它会为函数bar()生成新的栈帧,然后又调用一个 PyEval_EvalFrameEx() 运行bar()对应的字节码,……,如此递归,然后一层层的返回;

    2. 对于python中栈帧:

    在python中的栈帧其实是在解释器的堆上分配内存的,所以,在一个python函数运行完成后,它的栈帧的仍然存在,并没有消失,下面例子说明了(当func函数运行完成后,我们然后可以访问到它对应的栈帧):

    >>> import inspect
    >>> def func():
    ...   global x
    ...   x = inspect.currentframe()
    ... 
    >>> x = None
    >>> func()
    >>> x
    <frame object at 0x7f50f3ee2868>
    >>> x.f_code.co_name
    'func'

    3. python中的生成器函数是怎么运行的?

    #这是一个函数
    >>> def func():
    ...   print('You are SB')
    ... 
    #这是一个生成器
    
    >>> def gen():
    ...   yield 'You are SB'
    ...   return 'ni gei wo gun'

    对于函数与生成器函数的区别在于生成器中有yield表达式,  它们的co_flags是不相同的:

    function没有*args或**kw时,func.__code__.co_flags=67;           function有*args没有**kw时,func.__code__.co_flags=71;

    function没有*args有**kw时,func.__code__.co_flags=75;           function既有*args也有**kw时,func.__code__.co_flags=79;

    function是一个generator时,func.__code__.co_flags=99.

    >>> func.__code__.co_flags
    67
    >>> gen.__code__.co_flags
    99

    当运行一个生成器函数时,它会生成一个生成器:

    >>> a = gen()
    >>> type(a)
    <class 'generator'>
    >>> b= gen()
    >>> b
    <generator object gen at 0x7f50f4a7a3f0>

    上面例子中生成了两个生成器a与b, 每一个生成器都有两个常用的属性,分别为gi_frame与gi_code, 不同的生成器的gi_code是相同的,对应生成器函数的字节码,然而它们的gi_frame是不相同的,所以,不同的生成器可以分别运行,并且互不干扰;

    对于每一个栈帧又都有一个指针f_lasti,它指向了最后执行的命令,在一开始没有执行时,它的值为-1;

    >>> a.gi_frame.f_lasti
    -1
    >>> a.send(None)
    'You are SB'
    >>> a.gi_frame.f_lasti
    3
    
    >>> b.gi_frame.f_lasti
    -1

    当生成器执行到最后时,它就产生一个 StopIteration 异常,然后就停止了,当生成器函数中有return时, 这个异常的值就是return的值,如果没有return,异常的值为空;

    >>> next(b)
    'You are SB'
    >>> next(b)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration: ni gei wo gun

    生成器函数就就是这么运行的。

    4.生成器相关操作:

    1. X.__next__()方法和next()内置函数

        当我们调用一个生成器函数时来生成一个生成器X时,这个生成器对象就会自带一个X.__next__()方法,它可以开始或继续函数并运行到下一个yield结果的返回或引发一个StopIteration异常(这个异常是在运行到了函数末尾或着遇到了return语句的时候引起)。也可以通过python的内置函数next()来调用X.__next__()方法,结果都是一样的;

    >>> def gen():
    ...   yield 'NI'
    ...   return 'hahahaha'
    ...   yield 'HAO'
    ... 
    >>> x = gen()
    #查看一下x的属性,我们发现了__next__方法
    >>> dir(x)
    ['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'send', 'throw']
    
    #使用__next__方法运行函数;
    >>> x.__next__()
    'NI'
    >>> x.__next__()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration: hahahaha
    
    #使用内置的next()函数运行函数(重新生成一个生成器x)
    >>> x = gen()
    >>> next(x)
    'NI'
    >>> next(x)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration: hahahaha

    2. 生成器函数协议中的send()方法

        在讲send()方法的时候,有必要了解一下next()或__next__()或send()语句执行时,生成器内的程序执行到了哪里暂停了。写一个很简单的函数,使用pdb调试一下:

    #定义一个gen.py文件
     1 def gen():
      2   a = yield 1
      3   b = yield 2
      4   return 100
      5 
      6 x = gen()
      7 n1 = next(x)                                                                                                                               
      8 n2 = next(x)
    
    
    #使用pdb调试一下这个文件
    yinheyi@ubuntu:~/play$ python3.4 -m pdb gen.py
    > /home/yinheyi/play/gen.py(1)<module>()
    -> def gen():
    在第7行设置一个断点
    (Pdb) b 7             
    Breakpoint 1 at /home/yinheyi/play/gen.py:7
    #运行到断点前
    (Pdb) r                   
    > /home/yinheyi/play/gen.py(7)<module>()
    -> n1 = next(x)
    #此时,可以使用 l 查看一下状态,显示运行第7行了;
    (Pdb) l                  
      2        a = yield 1
      3        b = yield 2
      4        return 100
      5      
      6      x = gen()
      7 B->    n1 = next(x)
      8      n2 = next(x)
    # 查看一下变量 n1的值,应该还没有定义,因为还没有运行到;
    (Pdb) p n1
    *** NameError: name 'n1' is not defined
    # 查看一下生成器x的栈帧中的局部变量,应该是空,因为还没有开始执行生成器x
    (Pdb)p  x.gi_frame.f_locals
    {}
    # 执行第7行,使用next()开始执行了生成器x
    (Pdb) n
    > /home/yinheyi/play/gen.py(8)<module>()
    -> n2 = next(x)
    #再一次查看一个n1的值,它的值为1,即next( )的返回值,它的返回值就是第一个yield出来的值:1
    (Pdb) p n1
    1
    # 再一次 查看一下生成器x的栈帧中的局部变量,竞然还为空,说明了什么??已经执行了yield 1的表达式,但是这个表达式执行到 yield出来1就暂停了,并没有执行到生成表达式“yiled 1” 的返回值 为None;所以,局部变量里面没有值;
    (Pdb) p x.gi_frame.f_locals
    {}
    
    #那就再执行第8行语句,看看会怎么样?
    (Pdb) n
    --Return--
    > /home/yinheyi/play/gen.py(8)<module>()->None
    -> n2 = next(x)
    #打印 n2的值为2;
    (Pdb) p n2
    2
    #查看一下生成器x的栈帧中的局部变量,这时,发现有了变量a, 没有变量b, 明白了,原来如此
    (Pdb) p x.gi_frame.f_locals
    {'a': None}

        通过看上面的程序,我们知道,当next()或__next__()或send()语句执行时,在生成器里面的程序中它执行到 yiled value 这条语句,  它yield出来了一个value值,但是没有执行yiled value表达式 的返回值它就暂停了;

        现在说说send()方法:从技术上讲,yield是一个表达式,它是有返回值的,当我们使用内置的next()函数或__next__方法时,默认yield表达式的返回值为 None,它使用send(value)方法时,它可以把一个值传递给生成器,使得yield表达式的返回值为send()方法传入的值; 当我们第一次执行send()方法时,我们必须传入None值,因为第一次执行时,还没有等待返回值的yield表达式(虽然 send()方法会执行下一条yield语句,但是上面已经说明了它在还没有来得及执行yiled value表达式 的返回值时它就暂停了)

    定义一个gen.py文件,里面的内容为:
    1 def gen():                                                                                                                                 
      2   a = yield 1
      3   print('a的值为:', a)
      4   b = yield 2
      5   print('b的值为:', b)
      6   return '我要结束了'
      7 
      8 
      9 x = gen()
     10 print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
     11 n1 = x.send(None)
     12 print('第一个yield表达式yield出来的值为:', n1)
     13 print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
     14 n2 = x.send('love love love')
     15 print('第二个yield表达式yield出来的值为:', n2)
     16 print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
     17 try:
     18         n3 = x.send('TMDTMD')
     19 except StopIteration:
     20         print('我已经运行到末尾了,没有yield语句供我继续运行了')
     21 finally:
     22         print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
    
    
    #运行结果:
    yinheyi@ubuntu:~/play$ python3.4 gen.py 
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    第一个yield表达式yield出来的值为: 1
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    a的值为: love love love
    第二个yield表达式yield出来的值为: 2
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    b的值为: TMDTMD
    我已经运行到末尾了,没有yield语句供我继续运行了
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    3. 生成器函数中的return 语句:

        当生成器运行到了return语句时,会抛出StopIteration的异常,异常的值就是return的值;   另外,即使return后面有yield语句,也不会被执行;

    >>> def gen():
    ...   yield 'NI'
    ...   return 'hahahaha'
    ...   yield 'HAO'
    ... 
    >>> x = gen()
    >>> x.__next__()
    'NI'
    >>> x.__next__()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration: hahahaha

    4. 另外,一个生成器对象也有close方法与throw方法,可以使用它们提前关闭一个生成器或抛出一个异常;使用close方法时,它本质上是在生成器内部产生了一个终止迭代的GeneratorExit的异常;

    # 使用 close方法提前关闭异常;
    >>> x = gen()
    >>> x.close()
    >>> x.__next__()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration
    
    #使用throw方法抛出异常
    >>> x = gen()
    >>> x.throw(StopIteration)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 1, in gen

    5. 最后一个要讲的内容:yield from

    这个是在python3.0以后新增加的内容,可以让生成器delegate另一个生成器;

    1. 举一个例子看看它是怎么往外 yield数据的???

    #生成器函数1
    >>> def fun():
    ...   yield 1
    ...   yield 2
    ...   return 'hello'
    ...   yield 3
    
    #生成器函数2
    >>> def call_fun():
    ...   yield 'a'
    ...   result = yield from fun()
    ...  print(result)
    ...   yield 'b'
    ...   yield 'c'
    
    #运行;
    >>> caller = call_fun()
    >>> caller.send(None)
    'a'
    >>> caller.send(None)
    1
    >>> caller.gi_frame.f_lasti                    #此时,查看一下caller的指针指向14
    14
    >>> caller.send(None)
    2
    >>> caller.gi_frame.f_lasti                   #此时caller的指针仍然是指向14,说明caller生成器遇到yield from时被阻塞了;
    14
    >>> caller.send(None)
    hello                                                    #说明了 yield from 表达式的返回值为生成器fun()中return的返回值;
    'b'
    >>> caller.gi_frame.f_lasti
    22
    >>> caller.send(None)
    'c'
    >>> caller.send(None)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration

    这个例子我们明白了两点:1. 当我们调用主生成器caller时,遇到yield from 时,它就会停下来,运行子生成器的程序, yield出来的数据就是子生成器里的数据;2. yield from 表达式的返回值为子生成器的return的值;

    2. 举个例子看看它是怎么通过 send()方法往里传递数据的?

    >>> def fun():
    ...   a = yield 1
    ...   print('yield 1的值为', a)
    ...   b = yield 2
    ...   print('yield 2 的值为', b)
    ...   return '子生成器完成,我要返回了'
    ... 
    >>> def call_fun():
    ...   x1 = yield 'a'
    ...   print('yield a 的值为', x1)
    ...   result = yield from fun()
    ...  print(result)
    ...   x2 = yield 'b'
    ...   print('yield b 的值为', x2)
    
    #一步步运行;
    >>> caller = call_fun()
    >>> caller.send(None)
    'a'
    >>> caller.send('xiaoming')
    yield a 的值为 xiaoming
    1
    >>> caller.send('xiao')
    yield 1的值为 xiao
    2
    >>> caller.send('ming')
    yield 2 的值为 ming
    子生成器完成,我要返回了
    'b'
    >>> caller.send('hahahha')
    yield b 的值为 hahahha
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration

    通过这个例子,我们明白了1点:当主生成器遇到yield from以后,我们通过 send()方法传入值最终传给了子生成器;

    3. 通过 yield from ,可以嵌套调用生成器,比如:

    >>> def fun1():
    ...   yield 1
    ...   yield 2
    ... 
    >>> def fun2():
    ...   yield from fun1()
    ... 
    >>> def fun3():
    ...   yield from fun2()
    ... 
    >>> def fun4():
    ...   yield 'hello'
    ...   yield from fun3()
    ...   yield 'world'
    ... 
    
    #运行
    >>> a = fun4()
    >>> next(a)
    'hello'
    >>> next(a)
    1
    >>> next(a)
    2
    >>> next(a)
    'world'
    >>> next(a)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration

    部分内容参考:A Web Crawler With asyncio Coroutines中的内容;

    想要也了解更多,请参考python手册:https://docs.python.org/3/index.html

  • 相关阅读:
    GoLang之网络
    GoLang之方法与接口
    GoLang之基础
    Twemproxy 缓存代理服务器
    判断点是否在三角形内
    C++中const 的各种用法
    解决java web中safari浏览器下载后文件中文乱码问题
    Spring MVC如何测试Controller(使用springmvc mock测试)
    java生成指定范围的随机数
    itextpdf添加非自带字体(例如微软雅黑)
  • 原文地址:https://www.cnblogs.com/yinheyi/p/8087822.html
Copyright © 2011-2022 走看看