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

生成器 Generator 的定义

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

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



什么是Python的生成器 Generator

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

通俗地讲,

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



一个简单的生成器例子

直接看代码:

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行

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) 被第一次调用时, 自然从

 while num > 0  # num = 5

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

	yield = num

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



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

先讲中断执行, 因为yield 具有return 的效果, 所以当执行到yield 函数就退出了
print(next(g)) 会输出数字5

很容易理解

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

我们继续看回例子:

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行

yield num

时退出了

这时执行第5行

num -= 1

num 变成了4
由于这时代码在gen()里的while里
所以 代码会返回到第2行

while num >0

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

	yield num

这时再次中断 ,返回num

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

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



大部分情况下, yield 应该在gen()的循环体(例如while) 内使用

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

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

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个 生成器有两个yield, 只需要保证1个yield 在循环体内



生成器 可以有多个 yield

例子:

def generate_prime_numbers():
    count = 1
    list_primes = [2]  # 用于存储已发现的素数
    n = 3  # 从3开始检查奇数是否是素数
    yield list_primes[-1] # 首先输出第一个元素2
    while True:
        
        is_prime = True
        for prime in list_primes:
            if prime * prime > n:
                break
            if n % prime == 0:
                is_prime = False
                break

        if is_prime:
            list_primes.append(n)
            yield list_primes[-1] # 从第2次 到 第n次 在这里输出
            count += 1

        n += 2  # 只检查奇数是否是素数

    return # raise StopIteration



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

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

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

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

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个例子:

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

然后我们用

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里
如:

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

总结

好了到这里生成器的基本特性已经介绍完
我会在之后的文章里介绍生成器的一些常用使用场景

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nvd11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值