本节课纲
- 可迭代对象
- 迭代器
- 生成器
Python中内置的序列,如list、tuple、str、bytes、dict、set、collections.deque等都是可迭代对象,但它们不是迭代器。迭代器可以被 next() 函数调用,并不断返回下一个值。Python从可迭代的对象中获取迭代器。迭代器和生成器都是为了惰性求值(lazy evaluation),避免浪费内存空间,实现高效处理大量数据。在Python 3中,生成器有广泛的用途,所有生成器都是迭代器,因为生成器完全实现了迭代器接口。迭代器用于从集合中取出元素,而生成器用于"凭空"生成元素 。PEP 342 给生成器增加了 send() 方法,实现了"基于生成器的协程"。PEP 380允许生成器中可以return返回值,并新增了 yield from 语法结构,打开了调用方和子生成器的双向通道
1. 可迭代的对象
可迭代的对象(Iterable)是指使用iter()内置函数可以获取迭代器(Iterator)的对象。Python解释器需要迭代
对象x时,会自动调用iter(x)
,内置的iter()
函数有以下作用:
- 检查对象x是否实现了
__iter__()
方法,如果实现了该方法就调用它,并尝试获取一个迭代器
- 如果没有实现
__iter__()
方法,但是实现了__getitem__(index)
方法,尝试按顺序(从索引0开始)获取元素,即参数index是从0开始的整数(int)。之所以会检查是否实现__getitem(index)__
方法,为了向后兼容 - 如果前面都尝试失败,Python会抛出
TypeError
异常,通常会提示'X' object is not iterable
(X类型的对象不可迭代),其中X是目标对象所属的类
具体来说,哪些是可迭代对象呢?
- 如果对象实现了能返回
迭代器
的__iter__()
方法,那么对象就是可迭代的 - 如果对象实现了
__getitem__(index)
方法,而且index参数是从0开始的整数(索引),这种对象也可以迭代的。Python中内置的序列
类型,如list、tuple、str、bytes、dict、set、collections.deque等都可以迭代,原因是它们都实现了__getitem__()
方法(注意: 其实标准的序列还都实现了__iter__()
方法)
1.1. 判断对象是否可迭代
从Python 3.4开始,检查对象x能否迭代,最准确的方法是:调用iter(x)
函数,如果不可迭代,会抛出TypeError
异常。这比使用isinstance(x, abc.Iterable)
更准确,因为iter(x)
函数会考虑到遗留的__getitem__(index)
方法,而abc.Iterable
类则不会考虑
1.2. getitem()
下面构造一个类,它实现了__getitem__()
方法。可以给类的构造方法传入包含一些文本的字符串,然后可以逐个单词进行迭代:
'''创建test.py模块'''
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __getitem__(self, index):
return self.words[index]
def __len__(self): # 为了让对象可以迭代没必要实现这个方法,这里是为了完善序列协议,即可以用len(s)获取单词个数
return len(self.words)
def __repr__(self):
return 'Sentence({})'.format(reprlib.repr(self.text))
测试Sentence
实例能否迭代:
In [1]: from test import Sentence # 导入刚创建的类
In [2]: s = Sentence('I love Python') # 传入字符串,创建一个Sentence实例
In [3]: s
Out[3]: Sentence('I love Python')
In [4]: s[0]
Out[4]: 'I'
In [5]: s.__getitem__(0)
Out[5]: 'I'
In [6]: for word in s: # Sentence实例可以迭代
...: print(word)
...:
I
love
Python
In [7]: list(s) # 因为可以迭代,所以Sentence对象可以用于构建列表和其它可迭代的类型
Out[7]: ['I', 'love', 'Python']
In [8]: from collections import abc
In [9]: isinstance(s, abc.Iterable) # 不能正确判断Sentence类的对象s是可迭代的对象
Out[9]: False
In [10]: iter(s) # 没有抛出异常,返回迭代器,说明Sentence类的对象s是可迭代的
Out[10]: <iterator at 0x7f82a761e5f8>
1.3. iter()
如果实现了__iter__()
方法,但该方法没有返回迭代器
时:
In [1]: class Foo:
...: def __iter__(self):
...: pass
...:
In [2]: from collections import abc
In [3]: f = Foo()
In [4]: isinstance(f, abc.Iterable) # 错误地判断Foo类的对象f是可迭代的对象
Out[4]: True
In [5]: iter(f) # 使用iter()方法会抛出异常,即对象f不可迭代,不能用for循环迭代它
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-5-a2fd621ca1d7> in <module>()
----> 1 iter(f)
TypeError: iter() returned non-iterator of type 'NoneType'
Python迭代协议要求iter()必须返回特殊的迭代器对象。下一节会讲迭代器,迭代器对象必须实现__next__()
方法,并使用StopIteration
异常来通知迭代结束
In [1]: class Foo:
...: def __iter__(self): # 其实是将迭代请求委托给了列表
...: return iter([1, 2, 3]) # iter()函数从列表创建迭代器,等价于[1, 2, 3].__iter__()
...:
In [2]: from collections import abc
In [3]: f = Foo()
In [4]: isinstance(f, abc.Iterable)
Out[4]: True
In [5]: iter(f)
Out[5]: <list_iterator at 0x7fbe0e4f2d30>
In [6]: for i in f:
...: print(i)
...:
1
2
3
1.4. iter()函数的补充
iter()
函数有两种用法:
-
iter(iterable) -> iterator
: 传入可迭代的对象
,返回迭代器
-
iter(callable, sentinel) -> iterator
: 传入两个参数,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符
下述示例展示如何使用iter()
函数的第2种用法来掷骰子,直到掷出 1 点为止:
In [1]: from random import randint
In [2]: def d6():
...: return randint(1, 6)
...:
In [3]: d6_iter = iter(d6, 1) # 第一个参数是d6函数,第二个参数是哨符
In [4]: d6_iter # 这里的 iter 函数返回一个 callable_iterator 对象
Out[4]: <callable_iterator at 0x473c5d0>
In [5]: for roll in d6_iter: # for 循环可能运行特别长的时间,不过肯定不会打印 1,因为 1 是哨符
...: print(roll)
...:
6
3
5
2
4
4
实用的示例: 逐行读取文件,直到遇到空行或者到达文件末尾为止
with open('mydata.txt') as fp:
for line in iter(fp.readline, '\n'): # fp.readline每次返回一行
print(line)
2. 迭代器
迭代是数据处理的基石。当扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator pattern)
迭代器
是这样的对象:实现了无参数的__next__()
方法,返回序列中的下一个元素,如果没有元素了,就抛出StopIteration
异常。即,迭代器可以被next()函数调用,并不断返回下一个值。
在 Python 语言内部,迭代器
用于支持:
- for 循环
- 构建和扩展集合类型
- 逐行遍历文本文件
- 列表推导、字典推导和集合推导
- 元组拆包
- 调用函数时,使用 * 拆包实参
2.1. 判断对象是否为迭代器
检查对象x是否为迭代器
最好的方式是调用 isinstance(x, abc.Iterator)
:
In [1]: from collections import abc
In [2]: isinstance([1,3,5], abc.Iterator)
Out[2]: False
In [3]: isinstance((2,4,6), abc.Iterator)
Out[3]: False
In [4]: isinstance({'name': 'wangy', 'age': 18}, abc.Iterator)
Out[4]: False
In [5]: isinstance({1, 2, 3}, abc.Iterator)
Out[5]: False
In [6]: isinstance('abc', abc.Iterator)
Out[6]: False
In [7]: isinstance(100, abc.Iterator)
Out[7]: False
In [8]: isinstance((x*2 for x in range(5)), abc.Iterator) # 生成器表达式,后续会介绍
Out[8]: True
Python中内置的序列类型,如list、tuple、str、bytes、dict、set、collections.deque等都是可迭代的对象,但不是迭代器; 生成器一定是迭代器
2.2. next()和iter()
标准的迭代器
接口:
-
__next__()
: 返回下一个可用的元素,如果没有元素了,抛出StopIteration
异常。调用next(x)
相当于调用x.__next__()
-
__iter__()
: 返回迭代器
本身(self),以便在应该使用可迭代的对象
的地方能够使用迭代器
,比如在for
循环、list(iterable)
函数、sum(iterable, start=0, /)
函数等应该使用可迭代的对象
地方可以使用迭代器
。说明: 如章节1所述,只要实现了能返回迭代器
的__iter__()
方法的对象就是可迭代的对象
,所以,迭代器都是可迭代的对象!
下面的示例中,Sentence
类的对象是可迭代的对象
,而SentenceIterator
类实现了典型的迭代器
设计模式:
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
return SentenceIterator(self.words) # 迭代协议要求__iter__返回一个迭代器
class SentenceIterator:
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index] # 获取 self.index 索引位(从0开始)上的单词。
except IndexError:
raise StopIteration() # 如果 self.index 索引位上没有单词,那么抛出 StopIteration 异常
self.index += 1
return word
def __iter__(self):
return self # 返回迭代器本身
2.3. next()函数获取迭代器中下一个元素
除了可以使用for
循环处理迭代器
中的元素以外,还可以使用next()
函数,它实际上是调用iterator.__next__()
,每调用一次该函数,就返回迭代器
的下一个元素。如果已经是最后一个元素了,再继续调用next()
就会抛出StopIteration
异常。一般来说,StopIteration
异常是用来通知我们迭代结束的:
with open('/etc/passwd') as fd:
try:
while True:
line = next(fd)
print(line, end='')
except StopIteration:
pass
或者,为next()
函数指定第二个参数(默认值),当执行到迭代器
末尾后,返回默认值,而不是抛出异常:
with open('/etc/passwd') as fd:
while True:
line = next(fd, None)
if line is None:
break
print(line, end='')
2.4. 可迭代的对象与迭代器的对比
首先,我们要明确可迭代的对象
和迭代器
之间的关系:Python从可迭代的对象中获取迭代器
比如,用for
循环迭代一个字符串'ABC',字符串是可迭代的对象
。for
循环的背后会先调用iter(s)
将字符串转换成迭代器
,只不过我们看不到:
In [1]: s = 'ABC'
In [2]: for char in s:
...: print(char)
...:
A
B
C
如果没有for
循环,就不得不使用while
循环来模拟:
In [3]: it = iter(s) # 使用可迭代的对象s构建迭代器it
In [4]: while True:
...: try:
...: print(next(it)) # 不断在迭代器上调用next函数,获取下一个字符
...: except StopIteration: # 如果没有字符了,迭代器会抛出StopIteration异常
...: del it
...: break
...:
A
B
C
StopIteration异常表明迭代器到头了,Python语言内部会处理for循环和其它迭代上下文(如列表推导、元组拆包等)中的StopIteration异常
使用章节2.2中定义的Sentence
类,演示如何使用iter()
函数来构建迭代器
,并使用next()
函数依次获取迭代器
中的元素:
In [1]: from test import Sentence
In [2]: s = Sentence('Pig and Pepper')
In [3]: it = iter(s) # 获取迭代器
In [4]: it
Out[4]: <iterator at 0x4148650>
In [5]: next(it) # 使用next()方法获取下一个单词
Out[5]: 'Pig'
In [6]: it.__next__() # __next__()方法也能达到效果,但我们应该避免直接调用特殊方法
Out[6]: 'and'
In [7]: next(it)
Out[7]: 'Pepper'
In [8]: next(it) # 没有单词了,因此迭代器抛出StopIteration异常
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-8-bc1ab118995a> in <module>()
----> 1 next(it)
StopIteration:
In [9]: list(it) # 到头后,迭代器就没用了
Out[9]: []
In [10]: list(iter(s)) # 如果想再次迭代,要重新构建迭代器
Out[10]: ['Pig', 'and', 'Pepper']
总结:
-
迭代器
要实现__next__()
方法,返回迭代器
中的下一个元素 -
迭代器
还要实现__iter__()
方法,返回迭代器
本身,因此,迭代器
可以迭代。迭代器
都是可迭代的对象
-
可迭代的对象
一定不能是自身的迭代器
。也就是说,可迭代的对象
必须实现__iter__()
方法,但不能实现__next__()
方法
3. 生成器
在Python中,可以使用生成器
让我们在迭代的过程中不断计算后续的值,而不必将它们全部存储在内存中:
'''斐波那契数列由0和1开始,之后的费波那契系数就是由之前的两数相加而得出,它是一个无穷数列'''
def fib(): # 生成器函数
a, b = 0, 1
while True:
yield a
a, b = b, a + b
g = fib() # 调用生成器函数,返回一个实现了迭代器接口的生成器对象,生成器一定是迭代器
counter = 1
for i in g: # 可以迭代生成器
print(i) # 每需要一个值时,才会去计算生成
counter += 1
if counter > 10: # 只生成斐波那契数列前10个数值
break
3.1. 生成器函数
只要 Python 函数的定义体中有 yield
关键字,该函数就是生成器函数
。调用生成器函数
时,会返回一个生成器(generator)
对象。也就是说,生成器函数是生成器工厂
普通的函数与生成器函数
在语法上唯一的区别是,在后者的定义体中有 yield
关键字
In [1]: def gen_AB(): # 定义生成器函数的方式与普通的函数无异,只不过要使用 yield 关键字
...: print('start')
...: yield 'A'
...: print('continue')
...: yield 'B'
...: print('end')
...:
In [2]: gen_AB # 生成器函数
Out[2]: <function __main__.gen_AB()>
In [3]: g = gen_AB() # 调用生成器函数,返回一个生成器对象,注意:此时并不会执行生成器函数定义体中的代码,所以看不到打印start
In [4]: g
Out[4]: <generator object gen_AB at 0x04CA74E0>
In [5]: next(g) # 生成器都是迭代器,执行next(g)时生成器函数会向前,前进到函数定义体中的下一个 yield 语句,生成 yield 关键字后面的表达式的值,在函数定义体的当前位置暂停,并返回生成的值
start
Out[5]: 'A'
In [6]: next(g)
continue
Out[6]: 'B'
In [7]: next(g)
end
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-7-e734f8aca5ac> in <module>()
----> 1 next(g)
StopIteration:
调用生成器函数
后会创建一个新的生成器
对象,但是此时还不会执行函数体。
第一次执行next(g)
时,会激活生成器
,生成器函数
会向前 前进到 函数定义体中的 下一个 yield 语句,生成 yield
关键字后面的表达式的值
,在函数定义体的当前位置暂停
,并返回生成的值。具体为:
- 执行
print('start')
输出start - 执行
yield 'A'
,此处yield
关键字后面的表达式为'A'
,即表达式的值为A。所以整条语句会生成值A,在函数定义体的当前位置暂停
,并返回值A,我们在控制台上看到输出A
第二次执行next(g)
时,生成器函数定义体中的代码由 yield 'A'
前进到 yield 'B'
,所以会先输出continue,并生成值B,又在函数定义体的当前位置暂停
,返回值B
第三次执行next(g)
时,由于函数体中没有另一个 yield
语句,所以前进到生成器函数的末尾,会先输出end。到达生成器函数定义体的末尾时,生成器对象抛出StopIteration
异常
注意用词: 普通函数返回值,调用生成器函数返回生成器,生成器产出或生成值
调用
生成器函数
后,会构建一个实现了迭代器
接口的生成器
对象,即,生成器一定是迭代器!
In [8]: for c in gen_AB():
...: print('-->', c)
...:
start
--> A
continue
-->
end
所以,可以使用生成器函数
改写前面章节中的Sentence
类,此时不再需要SentenceIterator
类:
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
# 最简单是委托迭代给列表,这里仅演示生成器函数的用法
# return iter(self.words) # 等价于self.words.__iter__()
for word in self.words:
yield word # 产出当前的word
迭代器和生成器都是为了惰性求值(lazy evaluation),避免浪费内存空间。而上面的Sentence
类却不具备惰性
,因为RE_WORD.findall(text)
会创建所有匹配项的列表
,然后将其绑定到 self.words
属性上。如果我们传入一个非常大的文本,那么该列表
使用的内存量可能与文本本身一样多,而假设我们只需要迭代前几个单词,那么将浪费大量的内存。
re.finditer
函数是 re.findall
函数的惰性版本,返回的不是列表
,而是一个迭代器
,按需生成 re.MatchObject
实例。如果有很多匹配, re.finditer
函数能节省大量内存。我们要使用这个函数让 Sentence
类变得懒惰,即只在需要时才生成下一个单词:
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
# finditer()函数构建一个迭代器,包含 self.text 中匹配 RE_WORD 的单词,产出 MatchObject 实例
for match in RE_WORD.finditer(self.text):
yield match.group() # match.group() 方法从 MatchObject 实例中提取匹配正则表达式的具体文本
3.2. 生成器表达式
简单的生成器函数
(有yield
关键字),可以替换成生成器表达式
(没有yield
关键字,将列表推导
中的[]
替换为()
即可),让代码变得更简短
生成器表达式
可以理解为列表推导
的惰性版本:不会迫切地构建列表
,而是返回一个生成器
,按需惰性生成元素。也就是说,如果列表推导
是制造列表
的工厂,那么生成器表达式
就是制造生成器
的工厂:
In [1]: def gen_AB():
...: print('start')
...: yield 'A'
...: print('continue')
...: yield 'B'
...: print('end')
...:
In [2]: res1 = [x*3 for x in gen_AB()] # 列表推导迫切地迭代 gen_AB() 函数生成的生成器对象产出的元素: 'A' 和 'B'。注意,下面的输出是 start、 continue 和 end
start
continue
end
In [3]: for i in res1: # 这个 for 循环迭代列表推导生成的 res1 列表
...: print('-->', i)
...:
--> AAA
--> BBB
In [4]: res2 = (x*3 for x in gen_AB()) # 把生成器表达式返回的值赋值给 res2。只需调用 gen_AB() 函数,虽然调用时会返回一个生成器,但是这里并不使用
In [5]: res2 # res2 是一个生成器对象
Out[5]: <generator object <genexpr> at 0x04599330>
In [6]: for i in res2: # 只有 for 循环迭代 res2 时, gen_AB 函数的定义体才会真正执行。 for 循环每次迭代时会隐式调用 next(res2),前进到 gen_AB 函数中的下一个 yield 语句。注意, gen_AB 函数的输出与 for 循环中 print 函数的输出夹杂在一起
...: print('-->', i)
...:
start
--> AAA
continue
--> BBB
end
可以看出,生成器表达式会产出生成器,因此可以使用生成器表达式
进一步减少Sentence
类的代码:
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self): # 不再是生成器函数了(没有 yield),而是使用生成器表达式构建生成器
return (match.group() for match in RE_WORD.finditer(self.text))
何时使用
生成器表达式
?
生成器表达式
是创建生成器
的简洁语法,这样无需先定义函数再调用。不过,生成器函数
灵活得多,可以使用多个语句实现复杂的逻辑,也可以作为协程
使用(后续博文介绍)
遇到简单的情况时,可以使用生成器表达式
,因为这样扫一眼就知道代码的作用。如果生成器表达式
要分成多行写,我倾向于定义生成器函数
,以便提高可读性。此外,生成器函数
有名称,因此可以重用
如果将生成器表达式
传入只有一个参数的函数时,可以省略生成器表达式
外面的()
:
In [1]: list((x*2 for x in range(5)))
Out[1]: [0, 2, 4, 6, 8]
In [2]: list(x*2 for x in range(5)) # 可以省略生成器表达式外面的()
Out[2]: [0, 2, 4, 6, 8]
3.3. 嵌套的生成器
可以将多个生成器
像管道(pipeline)
一样链接起来使用,更高效的处理数据:
In [1]: def integers(): # 1\. 产出整数的生成器
...: for i in range(1, 9):
...: yield i
...:
In [2]: chain = integers()
In [3]: list(chain)
Out[3]: [1, 2, 3, 4, 5, 6, 7, 8]
In [4]: def squared(seq): # 2\. 基于整数的生成器,产出平方数的生成器
...: for i in seq:
...: yield i * i
...:
In [5]: chain = squared(integers())
In [6]: list(chain)
Out[6]: [1, 4, 9, 16, 25, 36, 49, 64]
In [7]: def negated(seq): # 3\. 基于平方数的生成器,产出负的平方数的生成器
...: for i in seq:
...: yield -i
...:
In [8]: chain = negated(squared(integers())) # 链式生成器,更高效
In [9]: list(chain)
Out[9]: [-1, -4, -9, -16, -25, -36, -49, -64]
由于上面各生成器函数
的功能都非常简单,所以可以使用生成器表达式
进一步优化链式生成器
:
In [1]: integers = range(1, 9)
In [2]: squared = (i * i for i in integers)
In [3]: negated = (-i for i in squared)
In [4]: negated
Out[4]: <generator object <genexpr> at 0x7f2a5c09be08>
In [5]: list(negated)
Out[5]: [-1, -4, -9, -16, -25, -36, -49, -64]
3.4. 增强生成器
Python 2.5 通过了 PEP 342 -- Coroutines via Enhanced Generators ,这个提案为生成器
对象添加了额外的方法和功能,其中最值得关注的是.send()
方法
与.__next__()
方法一样,.send()
方法使生成器
前进到下一个yield
语句。不过,.send()
方法还允许调用方
把数据发送给生成器
,即不管传给.send()
方法什么参数,那个参数都会成为生成器函数
定义体中对应的yield表达式
的值。也就是说,.send()
方法允许在调用方
和生成器
之间双向交换数据,而.__next__()
方法只允许调用方
从生成器
中获取数据
查看生成器对象的状态:
可以使用 inspect.getgeneratorstate(...)
函数查看生成器
对象的当前状态:
-
'GEN_CREATED'
: 等待开始执行 -
'GEN_RUNNING'
: 正在被解释器执行。只有在多线程应用中才能看到这个状态 -
'GEN_SUSPENDED'
: 在yield
表达式处暂停 -
'GEN_CLOSED'
: 执行结束
In [1]: def echo(value=None):
...: print("Execution starts when 'next()' is called for the first time.")
...: try:
...: while True:
...: try:
...: value = (yield value) # 调用send(x)方法后,等号左边的value将被赋值为x
...: except Exception as e:
...: value = e
...: finally:
...: print("Don't forget to clean up when 'close()' is called.")
...:
In [2]: g = echo(1) # 返回生成器对象,此时value=1
In [3]: import inspect
In [4]: inspect.getgeneratorstate(g)
Out[4]: 'GEN_CREATED'
In [5]: print(next(g)) # 第一次要调用next()方法,让生成器前进到第一个yield处,后续才能在调用send()方法时,在该yield表达式位置接收客户发送的数据
Execution starts when 'next()' is called for the first time.
Out[5]: 1 # (yield value),产出value的值,因为此时value=1,所以打印1
In [6]: inspect.getgeneratorstate(g)
Out[6]: 'GEN_SUSPENDED'
In [7]: print(next(g)) # 第二次调用next()方法,相当于调用send(None),所以value = (yield value)中等号左边的value将被赋值为None。下一次While循环,又前进到(yield value)处,产出value的值,因为此时value=None,所以打印None
None
In [8]: inspect.getgeneratorstate(g)
Out[8]: 'GEN_SUSPENDED'
In [9]: print(g.send(2)) # 直接调用send(2)方法,所以value = (yield value)中等号左边的value将被赋值为2。下一次While循环,又前进到(yield value)处,产出value的值,因为此时value=2,所以打印2
2
In [10]: g.throw(TypeError, "spam") # 调用throw()方法,将异常对象发送给生成器,所以except语句会捕获异常,即value=TypeError('spam')。下一次While循环,又前进到(yield value)处,产出value的值,因为此时value=TypeError('spam'),所以打印TypeError('spam')
Out[10]: TypeError('spam')
In [11]: g.close() # 调用close()方法,关闭生成器
Don't forget to clean up when 'close()' is called.
In [12]: inspect.getgeneratorstate(g)
Out[12]: 'GEN_CLOSED'
这是一项重要的 "改进",甚至改变了生成器
的本性:像这样使用的话,生成器
就变身为基于生成器的协程
。
注意: 给已结束的生成器发送任何值,都将抛出StopIteration异常,且返回值(保存在异常对象的value属性上)是None
In [13]: g.send(3)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-35-494d69d54622> in <module>()
----> 1 g.send(3)
StopIteration:
3.5. yield from
yield from
是在Python3.3才出现的语法。所以这个特性在Python2中是没有的。
yield from
后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。
3.5.1. 简单应用:拼接可迭代对象
我们可以用一个使用yield
和一个使用yield from
的例子来对比看下。
使用yield
# 字符串
astr='ABC'
# 列表
alist=[1,2,3]
# 字典
adict={"name":"wangbm","age":18}
# 生成器
agen=(i for i in range(4,8))
def gen(*args):
for item in args:
for i in item:
yield i
new_list=gen(astr, alist, adict,agen)
print(list(new_list))
# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]
使用yield from
# 字符串
astr='ABC'
# 列表
alist=[1,2,3]
# 字典
adict={"name":"wangbm","age":18}
# 生成器
agen=(i for i in range(4,8))
def gen(*args):
for item in args:
yield from item
new_list=gen(astr, alist, adict, agen)
print(list(new_list))
# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]
由上面两种方式对比,可以看出,yield from后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰。
3.5.2. 复杂应用:生成器的嵌套
当 yield from
后面加上一个生成器后,就实现了生成的嵌套。
当然实现生成器的嵌套,并不是一定必须要使用yield from
,而是使用yield from
可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现。
讲解之前,首先要知道几个概念:
1、
调用方
:调用委派生成器的客户端(调用方)代码 2、委托生成器
:包含yield from表达式的生成器函数 3、子生成器
:yield from后面加的生成器函数
你可能不知道他们都是什么意思,没关系,来看下这个例子。
这个例子,是实现实时计算平均值的。 比如,第一次传入10,那返回平均数自然是10. 第二次传入20,那返回平均数是(10+20)/2=15 第三次传入30,那返回平均数(10+20+30)/3=20
# 子生成器
def average_gen():
total = 0
count = 0
average = 0
while True:
new_num = yield average
count += 1
total += new_num
average = total/count
# 委托生成器
def proxy_gen():
while True:
yield from average_gen()
# 调用方
def main():
calc_average = proxy_gen()
next(calc_average) # 预激下生成器
print(calc_average.send(10)) # 打印:10.0
print(calc_average.send(20)) # 打印:15.0
print(calc_average.send(30)) # 打印:20.0
if __name__ == '__main__':
main()
委托生成器的作用是:在调用方与子生成器之间建立一个双向通道
。
所谓的双向通道是什么意思呢? 调用方可以通过send()
直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。
你可能会经常看到有些代码,还可以在yield from
前面看到可以赋值。这是什么用法?
你可能会以为,子生成器yield回来的值,被委托生成器给拦截了。你可以亲自写个demo运行试验一下,并不是你想的那样。 因为我们之前说了,委托生成器,只起一个桥梁作用,它建立的是一个双向通道
,它并没有权利也没有办法,对子生成器yield回来的内容做拦截。
为了解释这个用法,还是用上述的例子,并对其进行了一些改造。
# 子生成器
def average_gen():
total = 0
count = 0
average = 0
while True:
new_num = yield average
if new_num is None:
break
count += 1
total += new_num
average = total/count
# 每一次return,都意味着当前协程结束。
return total,count,average
# 委托生成器
def proxy_gen():
while True:
# 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。
total, count, average = yield from average_gen()
print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))
# 调用方
def main():
calc_average = proxy_gen()
next(calc_average) # 预激协程
print(calc_average.send(10)) # 打印:10.0
print(calc_average.send(20)) # 打印:15.0
print(calc_average.send(30)) # 打印:20.0
calc_average.send(None) # 结束协程
# 如果此处再调用calc_average.send(10),由于上一协程已经结束,将重开一协程
if __name__ == '__main__':
main()
运行后,输出
10.0
15.0
20.0
计算完毕!!
总共传入 3 个数值, 总和:60,平均数:20.0
为什么要使用yield from
既然委托生成器,起到的只是一个双向通道的作用,还需要委托生成器做什么?调用方直接调用子生成器不就好啦?
下面我们来一起探讨一下,到底yield from 有什么过人之处,让我们非要用它不可。
因为它可以帮我们处理异常
如果我们去掉委托生成器,而直接调用子生成器。那我们就需要把代码改成像下面这样,我们需要自己捕获异常并处理。而不像使yield from
那样省心。
# 子生成器
def average_gen():
total = 0
count = 0
average = 0
while True:
new_num = yield average
if new_num is None:
break
count += 1
total += new_num
average = total/count
return total,count,average
# 调用方
def main():
calc_average = average_gen()
next(calc_average) # 预激协程
print(calc_average.send(10)) # 打印:10.0
print(calc_average.send(20)) # 打印:15.0
print(calc_average.send(30)) # 打印:20.0
# ----------------注意-----------------
try:
calc_average.send(None)
except StopIteration as e:
total, count, average = e.value
print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))
# ----------------注意-----------------
if __name__ == '__main__':
main()