Python中的迭代器、生成器和可迭代对象详解
本节课纲
- 可迭代对象
- 迭代器
- 生成器
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()
推荐阅读
-
探索 Python 中的迭代器和可迭代对象
-
在一篇文章中阅读 Python 生成器和迭代器
-
python 中的迭代和可迭代对象 代码示例
-
Python 高级 | 5 分钟了解迭代器和生成器,扎实的代码技能
-
带您了解 Python 中的生成器和迭代器的文章
-
Python 中的迭代器和列表解析是如何工作的?
-
Python 的列表生成器、生成器、可迭代对象和迭代器
-
深入掌握Python(第三部分):函数命名、作用域、命名空间、f-字符串、可迭代对象与迭代器详解
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面
-
Python里的迭代器和生成器:深度解析