Python中的Yield关键字

本文深入解析Python中的生成器和Yield关键字,通过对比列表生成式和生成器,阐述了生成器如何节省内存并动态生成值。同时,通过多个示例代码,详细讲解了Yield的工作原理和迭代过程,以及如何利用生成器控制资源访问。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Python中的Yield关键字

在理解yield之前,我们必须明白生成器(generators)是什么,在这之前我们先讲一下可迭代对象(iterables)

可迭代对象(iterables)

你可以创建一个列表,逐个读取列表项的过程叫做迭代(iteration)

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylist是可迭代的,当你使用列表生成式(list comprehension)的创建他的时候,且他是一个可迭代对象:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

你可以使用"for ... in ..."去遍历一个可迭代对象;listsstrings,files…
你可以很方便的读取可迭代对象的数据,
但是你把所有数据都保存在了内存中,当数据量非常大的时候,这就成了一个问题。

生成器(Generators)

生成器(Generators)是一个迭代器(iterators),一种你只能迭代一次的可迭代对象,生成器没有把所有值都放在内存里,而是动态的生成值

生成器代码和列表生成式一样,除了用()替代了[],但是你不能迭代生成器两次,下面上一个对比:

# 生成器
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...     print(i)
... 
0
1
4
# 在上面已经计算完了所有值,这里生成器已经是空的了
>>> for i in mygenerator:
...     print(i)
... 
# 列表生成式
>>> mygenerator = [x*x for x in range(3)]
>>> for i in mygenerator:
...     print(i)
... 
0
1
4
# 列表生成器可以再次迭代,还是生成一样的值
>>> for i in mygenerator:
...     print(i)
... 
0
1
4

生成器在每次调用的时候都计算一次值,第一次0,第二次1,第三次4,最后生成器为空。

Yield关键字

yield的和return关键字相似,不同的地方是yield关键字会返回一个生成器:

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

这个例子在真实世界并没有什么意义,但是当你知道你的函数要返回一个大量且只需要读取一次值的时候,用这种方法会更好。
要掌握yield你还必须要明白,当你调用方法的时候,你编写在方法内的代码并没有执行,这个函数只返回生成器对象:
例如

>>> def generator():
...     print('run')
...     yield 1
... 
>>> gen = generator()
>>> next(gen)
run
1

当你调用next的时候函数才真正运行。

生成器是怎么迭代的?

>>> def generator(a):
...     print('---1---')
...     while a:
...         print('---2---')
...         yield 'yield 2'
...         print('---3---')
...     print('---4---')
...     yield 'yield 2'
...     print('---5---')
...     yield 'yield 3'
... 

这里我们用输出信息来了解生成器的迭代过程
a=1

>>> gen = generator(1)
>>> for i in range(3):
...     print('i', i)
...     print(next(gen))
... 
i 0
---1---
---2---
yield 2
i 1
---3---
---2---
yield 2
i 2
---3---
---2---
yield 2

由上面的输出我们可以看到
第一次迭代generator(a)方法开始执行,首先输出了---1---然后进入了while语句内输出了---2---,遇到第一个yield关键字返回yield后面的值。
第二次迭代 从上次yield关键字后面开始执行输出---3------2---,遇到yield 关键字的时候返回值。
第三次迭代 同第二次迭代一样。

>>> gen = generator(0)
>>> for i in range(3):
...     print('i', i)
...     print(next(gen)) 
... 
i 0
---1---
---4---
yield 2
i 1
---5---
yield 3
i 2
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
StopIteration

这个时候代码不会执行到while循环里面
第一次迭代generator(a)方法开始执行,首先输出了---1---然后输出了---4---,遇到第一个yield关键字返回值。
第二次迭代从上次yield关键字后面开始执行输出---5---遇到yield 关键字的时候返回值
第三次迭代这个时候函数已经运行完了,生成器为空,抛出StopIteration,这里如果使用for ... in gen就不会抛出异常,因为当gen为空时for ... in ...就不会再做迭代了。

一个使用生成器控制资源访问的例子

>>> class Bank():   # 我们创建一个银行类
...    crisis = False
...    def create_atm(self):
...        while not self.crisis: 
...            yield "$100"
>>> hsbc = Bank()  # 银行类的实例
>>> corner_street_atm = hsbc.create_atm() # 创建一台atm,crisis 默认为False,可以取钱。
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True  # 危机发生了,crisis设置为True
>>> print(corner_street_atm.next())  # 原先的atm都取不出来钱了(生成器执行到了while语句,不符合条件,生成器里面没有值了)
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # 创建一台新的atm(这个时候返回的生成器仍然为空)
>>> print(wall_street_atm.next()) 
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # 危机过后
>>> print(corner_street_atm.next()) # 这个时候生成器已经是空的了,不会再执行代码,直接报错StopIteration
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # 建立一台新的atm恢复以前的业务
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

这里比较有趣的是,即使中间执行了hsbc.crisis = False,原先的生成器还是会抛异常,因为这个时候生成器已经为空了,不会再执行代码,所以 hsbc.crisis = False并没有影响到原来的corner_street_atm

斐波拉契数列(Fibonacci)生成器

>>> def fib():
...     n, a, b = 0, 0, 1
...     while True:
...         yield b
...         a, b = b , a+b

>>> gen = fib()
>>> next(gen)
1
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
3
>>> next(gen)
5

yield的经典用法,面试会考。

关于itertools

itertools模块包含操作iterables的特殊函数。 例如,复制一个生成器,链接两个生成器,使用单行分组嵌套列表中的值,使用 Map / Zip而不创建另一个列表,等等。
这里介绍一个用itertools.permutations去排列赛马可能到达的顺序

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

真实情况下我们需要排列组合的列表长度可能会很大,这个时候将排列组合的结果都放在内存中会占用很大的内存,实际上上周我实现Apriori算法的时候就干了这事,后面电脑就死机了。

这篇文章部分来自Stack Overflow上的高票回答,感兴趣可以直接移步Stack Overflow,如果文章中有错误欢迎指正,非常感谢。
What does the “yield” keyword do? — Stack Overflow

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值