Python系列 37 生成器

生成器

生成器(generator)是迭代器的一种特殊实现方式,有2种方式可以创建,一种是通过生成器函数创建,另一种是生成器表达式进行创建。

生成器继承了迭代器的一切优点,简而言之是迭代器的一种升级版本。

在前面的迭代器一章节中介绍过,如果要想获取一个迭代器,必须要先创建一个可迭代对象,然后调用其下的__iter__()方法才能拿到一个迭代器,这样做会产生一些问题,如下所示:

  • 我们只想要一个迭代器,并不需要可迭代对象的前提下该怎么办
  • 可迭代对象中的数据项会占据大量内存空间,如果要想获取1个含有1000万个数据项的列表迭代器,则必须先创建1个列表对象,再获取其专属的迭代器,虽然迭代器不耗费内存,但是可迭代对象必然耗费内存

而生成器则是简化了这种操作,只需要创建生成器函数就可以拿到一个单纯的迭代器。

如果要判断一个对象是否属于生成器,推荐使用collections.abc下的Generator进行判定:

>>> from collections.abc import Generator
>>> isinstance(list,Generator)
False

生成器函数

如果一个函数中出现yield关键字,则它就是一个生成器函数,当一个生成器函数加括号进行调用时,并不会立即运行逻辑体代码,而是返回一个生成器对象。

当对这个生成器对象调用next()方法时,将启动该生成器对象,生成器对象开始执行函数逻辑体代码。

而在执行函数逻辑体代码时如果碰见yield则会发生2件事情:

  • 返回yield后面的值,类似于return函数
  • 挂起当前生成器函数的运行状态,而不是结束生成器函数的运行,也就是说这个生成器对象不会被销毁

再次对生成器对象调用next()方法时恢复暂停状态,继续上述流程进行运行。

一个简单的例子:

def generatorFunction():
    n = 3
    while n:
        yield n
        n-=1
    # return None ❶

# ❷
generatorObject = generatorFunction()
print(generatorObject)

# ❸
print(next(generatorObject))
print(next(generatorObject))
print(next(generatorObject))

# ❹
print(next(generatorObject))

# <generator object generatorFunction at 0x10b5afdb0> 
# 3
# 2
# 1
# StopIteration

❶:默认的函数返回值即为None,在Python3之前的较低版本中,return关键字和yield关键字不可以同时出现在一个函数中,但是目前已经取消了这种设定

❷:生成器函数加括号,返回生成器对象

❸:生成器对象调用next()方法,开始执行函数体代码

❹:由next()方法抛出的StopIteration异常,并不是生成器对象抛出的

斐波拉契数列

下面的示例中将使用生成器函数获取斐波拉契数列中指定位数之前所有的数据项。

仅获取数据项,并不产生存储,因此使用生成器函数极为方便:

def getFibPositionValue(n):
    count, currentValue, nextValue = 0, 0, 1
    while count < n:
        yield currentValue
        currentValue, nextValue = nextValue, currentValue + nextValue
        count += 1


g = getFibPositionValue(30)
for i in g:
    print(i)

而如果是要对斐波拉契数列本身进行存储,使用普通函数则更好一点:

def createFibArray(n):
    fibArray = []
    count, currentValue, nextValue = 0, 0, 1
    while count < n:
        fibArray.append(currentValue)
        currentValue, nextValue = nextValue, currentValue + nextValue
        count += 1
    return fibArray


array = createFibArray(30)
print(array)

生成器表达式

生成器表达式类(generator expression)似于列表推导式,用于快速的创建一个生成器对象,使用()对表达式进行包裹:

genObject = (i for i in range(3))
print(genObject)

# <generator object <genexpr> at 0x10bed0db0>

如果外部已经拥有一个括号,则可以忽略这大括号,如将生成器对象转换为元组的完整写法如下:

tup = tuple((i for i in range(3)))
print(tup)

# (0, 1, 2)

忽略括号:

tup = tuple(i for i in range(3))
print(tup)

# (0, 1, 2)

send() close()

生成器的特性不仅如此,它还能够实现一种双向的生成器外部调用代码与生成器内部逻辑代码的信息交互功能。

因为yield不仅可以返回值,还可以接收值。

如下示例,外部通过send()方法发送的信息将被yield所接收,当整个生成器对象迭代完毕后,可以调用close()方法关闭这个生成器对象:

def generatorFunction():
    print("generator object run ...")
    firstRecvExternal = yield 1 # ①
    print(firstRecvExternal)  # 打印A

    secondRecvExternal = yield 2 # ②
    print(secondRecvExternal)  # 打印B

    lastRecvExternal = yield 3 # ③
    print(lastRecvExternal)  # 打印C


genObject = generatorFunction()

# ❶:启动生成器对象,执行函数,运行至①处返回 1,并暂停
genStartResult = genObject.send(None)
print(genStartResult)  # 打印1

# ❷:继续运行生成器对象,发送了字符串A,被生成器①处的firstRecvExternal所接收到,并进行了一次打印 A
# 直至运行至②处,返回结果2
genSecondSend = genObject.send("A")
print(genSecondSend)  # 打印2

# ❸:继续运行生成器对象,发送了字符串B,被生成器②处的secondRecvExternal所接收到,并且进行了一次打印 B
# 直至运行至③处,返回了结果3
genThirdSend = genObject.send("B")
print(genThirdSend) # 打印3

try:
    # 继续运行生成器函数,发送了字符串C,被生成器③处的lastRecvExternal所接收到,并且进行了一次打印 C
    # 然后再往下运行发现 return None,于是 __next__()函数抛出了 StopIteration 的异常,但是被这里处理了
    genLastSend = genObject.send("C")  # StopIteration
except StopIteration:
    # 处理异常,关闭生成器对象
    genObject.close()  

# generator object run ...
# 1
# A
# 2
# B
# 3
# C

yield form

生成器函数加括号不会执行函数体内部代码,而是返回生成器对象,生成器对象的启动必须通过next()或者send(None)。

for循环底层会调用生成器对象的__next__()方法进行启动和向下运行,因此通过for循环来操纵生成器是十分方便的:

def genFunction():
    count = 0
    while 1:
        if count < 10:
            yield count
        else:
            return
        count += 1

for i in genFunction(): # ❶
    print(i)

❶:首先将生成器函数转换为生成器对象,然后通过for不断的进行调用

如果是2个嵌套调用的生成器函数,外部嵌套生成器函数需要时刻yield子调用生成器函数的值,似乎用for循环来完成这个需求是最好的选择:

def outer():
    yield "run outer..."
    for item in inner():  # ❶
        yield item


def inner():
    yield "run inner 1 ..."
    yield "run innner 2 ..."
    yield "run inner 3 ..."

for item in outer():
    print(item)

# run outer...
# run inner 1 ...
# run innner 2 ...
# run inner 3 ...

❶:首先将生成器函数转换为生成器对象,然后通过for不断的进行调用

在Python3.3版本之后,你有了新的选择,使用yield from关键字,yield from关键字必须定义在一个生成器函数之中,它将会自动的在底层运行for循环进行调用另一个生成器函数,并将另一个生成器函数的结果进行返回:

def outer():
    yield "run outer..."
    yield from inner()  # ❶


def inner():
    yield "run inner 1 ..."
    yield "run innner 2 ..."
    yield "run inner 3 ..."

for item in outer():
    print(item)

# run outer...
# run inner 1 ...
# run innner 2 ...
# run inner 3 ...

❶:yield from关键字底层就是for循环,所以这里的代码会更加的精简

yield from关键字后面必须跟上一个可迭代对象,如迭代器、生成器:

def outer():
    yield "run outer..."
    yield from range(3)


for item in outer():
    print(item)

# run outer...
# 0
# 1
# 2

生成器函数栈帧

生成器函数为什么能够挂起当前状态?普通函数为什么不可以?其实根本原因还是在栈帧上。

普通的函数栈帧组成图:

image-20210521181510918

生成器函数栈帧组成图:

image-20210521181637987

函数栈帧结构体

一个函数其实完整的栈帧是由很多部分组成的,参见CPython源码,感兴趣的可以研究一下:

typedef struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;      /* previous frame, or NULL */
    PyCodeObject *f_code;       /* code segment */
    PyObject *f_builtins;       /* builtin symbol table (PyDictObject) */
    PyObject *f_globals;        /* global symbol table (PyDictObject) */
    PyObject *f_locals;         /* local symbol table (any mapping) */
    PyObject **f_valuestack;    /* points after the last local */
    /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.
       Frame evaluation usually NULLs it, but a frame that yields sets it
       to the current stack top. */
    PyObject **f_stacktop;
    PyObject *f_trace;          /* Trace function */
    char f_trace_lines;         /* Emit per-line trace events? */
    char f_trace_opcodes;       /* Emit per-opcode trace events? */
 
    /* Borrowed reference to a generator, or NULL */
    PyObject *f_gen;
 
    int f_lasti;                /* Last instruction if called */
    /* Call PyFrame_GetLineNumber() instead of reading this field
       directly.  As of 2.3 f_lineno is only valid when tracing is
       active (i.e. when f_trace is set).  At other times we use
       PyCode_Addr2Line to calculate the line from the current
       bytecode index. */
    int f_lineno;               /* Current line number */
    int f_iblock;               /* index in f_blockstack */
    char f_executing;           /* whether the frame is still executing */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
} PyFrameObject;

本章总结

这一章可以说及其重要,Python的协程编程中本章知识是绝对的核心基础,只有理解了生成器才用可能理解协程。

还是以问答形式进行记录吧:

①:生成器和迭代器有什么不同?

答:生成器就是迭代器的另一种实现,普通的迭代器必须通过iter()函数进行创建,也就是说普通的迭代器必须依赖可迭代对象才能够创建出来,但是可迭代对象会占用大量内存,所以产生了生成器,即不通过可迭代对象就能拿到的迭代器。

②:yield form的作用?

答:yield form只能定义在一个生成器函数中,该生成器函数内部如果嵌套调用了另一个生成器函数时就可以使用yield form进行另一个生成器函数的调用,它底层会自动经过for循环对另一个生成器对象进行操纵,且将值返回给最外部。

③:生成器函数和普通函数有什么不同?

答:生成器函数加括号得到生成器对象,生成器对象调用其下__next__()方法才会执行函数体代码。而普通函数加括号直接执行函数体代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值