入门理解python中的generator - 生成器

生成器 Generator 的定义

生成器(Generator)是一种特殊的函数,可以用于迭代地生成一系列值,而不需要一次性生成所有值并将它们存储在内存中。生成器在需要时逐个生成值,并在生成值后暂停执行,保留函数的状态,以便下次调用时能够从停止的地方继续执行。

生成器函数使用 yield 语句来定义,而不是常规函数中的 return 语句。当生成器函数被调用时,它返回一个生成器对象,而不是立即执行函数体。每次调用生成器对象的 next() 方法时,生成器函数将从上次执行停止的位置继续执行,直到遇到下一个 yield 语句。生成器通过生成一个值并将其返回给调用者来实现迭代。

什么是Python的生成器 Generator

上面的定义 过于官方, 理解有点困难。

通俗地讲,

  1. 生成器就是一种特别的迭代器(iterator)。
  2. 生成器的大部分使用方法是跟迭代器一样的
  3. 生成器代码更简洁, 而且具有临时修改当前元素的功能

一个简单的生成器例子

直接看代码:

python 复制代码
def gen(num):
    while num > 0:
        yield num # return num to next() and pause the function
        num -= 1
    return # raise StopIteration


g = gen(5) # g is a generator object

print(next(g)) # 5
print(next(g)) # 4
print(next(g)) # 3

for i in g:
    print(i) # 2 1
简单代码解释

1 到 5 行是 定义1个 生成器函数 gen()

第8行是基于生成器函数 定义了 了1个生成器对象 g

之后下面的代码就是可以把 g 当成iterator 来使用了, 遍历它的元素。

初步理解生成器函数

首先, 生成器 与 迭代器 1个明显的区别是, 迭代器是用类来定义的, 迭代器器的对象必须实现__next__ 函数

而且生成器是用 函数来定义的, 而里面的关键字就是 yield

一旦python 中某个函数 写入了 yield 这个关键字, 则代表python 不会把这个函数当作普通函数, 而且1个种特别的函数 - 生成器函数

生成器的函数的

yield 可以理解为return 返回1个值

而生成器最后的return 可不是普通 函数 return的意思, 实际是代表 raise StopIteration, 没有后面的元素的了的意思。

初步理解生成器对象

我们再看看上面例子的第8行

python 复制代码
def gen(num):
    while num > 0:
        yield num # return num to next() and pause the function
        num -= 1
    return # raise StopIteration


g = gen(5)

g = gen(5) 如果按照一般函数的思维 g 肯定是 gen() 的返回值, 由于gen()里的return 后面是吉的, 所以 g = None 吗?

实际上, 由于gen() 里面包含关键字yield, 所以它是1个生成器函数, 所以执行这g = gen(5) 时, 实际上gen()里面的代码一句都还没开始执行。

而且上面说了, gen() 里面的return 并不是真正的return , 就算我们写成 return 100, g 也不会是100!

所以我们可以认为 g = gen(5) 简单看为定义了1个g 生成器对象。

生成器函数里的代码什么时候被执行

实际上, 生成器的代码只会在 生成器对象的 next 函数调用时 才开始执行

例如上面例子中的

print(next(g))

这时, gen(5) 开始被调用

当执行到yield 时到底发生了什么

继续上面, 当gen(5) 被第一次调用时, 自然从

python 复制代码
 while num > 0  # num = 5

这句代码开始执行, 由于num 初始值为5, 这是由 g = gen(5) 定义时决定的。

当执行到下一句代码

python 复制代码
	yield = num

这时 gen() 函数会把num return 出去

所以 print(next(g)) 就会在console 里输出 num的值 5了, 这就是为何上面说的 yield 具有普通函数 return的功能

yield 还会中断生成器函数的执行, 并保存为下次执行的开始位置

先讲中断执行, 因为yield 具有return 的效果, 所以当执行到yield 函数就退出了

print(next(g)) 会输出数字5

很容易理解

关键是保存当前的位置,并不是很容易理解

我们继续看回例子:

python 复制代码
def gen(num):
    while num > 0:
        yield num # return num to next() and pause the function
        num -= 1
    return # raise StopIteration


g = gen(5) # g is a generator object

print(next(g)) # 5
print(next(g)) # 4

这时, 第10行已经执行完成, 开始执行第11行的

print(next(g))

而这时, gen() 函数会从第5行开始执行(因为上次执行执行到 第4行

python 复制代码
yield num

时退出了

这时执行第5行

python 复制代码
num -= 1

num 变成了4

由于这时代码在gen()里的while里

所以 代码会返回到第2行

py 复制代码
while num >0

这时num的值为4, 还是大于0, 再执行第3行

py 复制代码
	yield num

这时再次中断 ,返回num

所以这时 print(next(g))就会在console 输出数字4了

关键, 当gen生成器函数 被再次调用, 它实际上是应该在上一次调用yield 的下一行开始执行

yield 应该在gen()的循环体(例如while) 内使用

很明显, 想保存 生成器的函数执行位置 , yield 应该在循环体内调用, 否则就直接 到了return # raise StopInteration , 失去了迭代元素的功能

例如:

上面例子可以由while 循环改成for 循环

python 复制代码
def gen(num): # from num to 1,  not includes 0
    for i in range(num, 0, -1):
        yield i
    return # raise StopIteration


g = gen(5) # g is a generator object

print(next(g)) # 5
print(next(g)) # 4
print(next(g)) # 3

for i in g:
    print(i) # 2 1

生成器 与 迭代器 保存当前元素的方法 的区别

由于生成器和迭代器的使用方法几乎相同。

他们都可以在遍历的过程中保存当前的元素

但是迭代器是保存在迭代器对象里的1个属性当中的。

例如:

python 复制代码
class LinkListIterator:
    def __init__(self, _first_node) -> None:
        self._current_node = _first_node

    def __iter__(self):
        return self
    
    def __next__(self):
        if not self._current_node:
            raise StopIteration
        current = self._current_node
        self._current_node = current.next
        return current.value

中的self._current_node

而且生成器是利用yield 关键字保存在python 的框架中的.

生成器 支持用send() 方法临时修改当前保存的元素

我们看1个例子:

python 复制代码
def gen(num):
    while num > 0:
        tmp = yield num # return num to next() and pause the function
        if tmp:
            num = tmp
        
        num -= 1
    return # raise StopIteration

g = gen(5)
print(next(g)) # 5
print(next(g)) # 4
print(next(g)) # 3

print(g.send(None)) # 2 # equal to next(g)
print(g.send(10)) # 9

print("=============================================")

for i in g:
    print(i) # 8 7 6 5 4 3 2 1

上面例子中, 我们使用了1个变量tmp 去保存 yield num的输出

然后我们就可以用g.send() 去修改这个这个tmp

然后我们用

python 复制代码
if tmp:
	num = tmp

去修改num 的值, 就是实现了 临时修改 生成器步骤的功能

看例子 在15 行, g.send(None) 效果就是send 1个None 给tmp, 并不会修改num 的值, 所以这里的g.send(None) 等同于 next(g)

关键来了, 在第16行, 我们调用g.send(10) 为何输出的是9 而不是10?

当我们调用g.send(10) 时, gen() 函数会在上次调用yield 的下一行执行, 就是从第4行 if tmp:

但是第7行num-=1 会被执行, 所以下次yield输出就是9了

如果向避免这种gap可以把 num -= 1 写进else 的block里

如:

python 复制代码
def gen(num):
    while num > 0:
        tmp = yield num # return num to next() and pause the function
        if tmp:
            num = tmp
        else:
            num -= 1
    return # raise StopIteration

g = gen(5)
print(next(g)) # 5
print(next(g)) # 4
print(next(g)) # 3

print(g.send(None)) # 2 # equal to next(g)
print(g.send(10)) # 10

print("=============================================")

for i in g:
    print(i) # 9 8 7 6 5 4 3 2 1

总结

好了到这里生成器的基本特性已经介绍完

我会在之后的文章里介绍生成器的一些常用使用场景

相关推荐
FreakStudio1 小时前
全网最适合入门的面向对象编程教程:56 Python字符串与序列化-正则表达式和re模块应用
python·单片机·嵌入式·面向对象·电子diy
丶21361 小时前
【CUDA】【PyTorch】安装 PyTorch 与 CUDA 11.7 的详细步骤
人工智能·pytorch·python
_.Switch2 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一个闪现必杀技2 小时前
Python入门--函数
开发语言·python·青少年编程·pycharm
小鹿( ﹡ˆoˆ﹡ )2 小时前
探索IP协议的神秘面纱:Python中的网络通信
python·tcp/ip·php
卷心菜小温3 小时前
【BUG】P-tuningv2微调ChatGLM2-6B时所踩的坑
python·深度学习·语言模型·nlp·bug
陈苏同学3 小时前
4. 将pycharm本地项目同步到(Linux)服务器上——深度学习·科研实践·从0到1
linux·服务器·ide·人工智能·python·深度学习·pycharm
唐家小妹3 小时前
介绍一款开源的 Modern GUI PySide6 / PyQt6的使用
python·pyqt
羊小猪~~4 小时前
深度学习项目----用LSTM模型预测股价(包含LSTM网络简介,代码数据均可下载)
pytorch·python·rnn·深度学习·机器学习·数据分析·lstm
Marst Code4 小时前
(Django)初步使用
后端·python·django