前言
- 作为python程序员,生成器以及协程是必不可少的话题。你可能在面试中会经常遇到这样的问题:说一说生成器和迭代器的区别?使用了哪些异步插件?讲一讲asyncio的用法以及原理?等等。当然,能回答出这些问题只是初级目标,重要的是,我们是否深入掌握了这些内容,是否在实际中能够找到合适的方法处理异步问题。我依照《fluent pyhton》中的经典例子,结合我自己的理解,由浅入深讲解。本来只打算写一篇文章的,但是写着写着发现内容过多,只好拆分出来。
iter(...) 函数如何把序列变得可以迭代
import re
import reprlib
RE_WORD = re.compile('\w+')
class Sentence:
def __init__(self, text):
self.text = text
# re.findall 函数返回一个字符串列表,里面的元素是正则表达式的全部非重叠匹配。
self.words = RE_WORD.findall(text)
def __getitem__(self, item):
return self.words[item]
def __len__(self):
return len(self.words)
def __repr__(self):
# reprlib.repr 这个实用函数用于生成大型数据结构的简略字符串表示形式
# 默认情况下,reprlib.repr 函数生成的字符串最多有 30 个字符
return "Sentence({})".format(reprlib.repr(self.text))
Sentence 实例测试
if __name__ == '__main__':
s = Sentence('"The time has come," the Walrus said,')
print(s)
for word in s:
print(word)
print(list(s))
output
Sentence('"The time ha... Walrus said,')
The
time
has
come
the
Walrus
said
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
- 通过测试说明Sentence实例可迭代,实现了序列协议,但是为什么可迭代呢?
序列可以迭代的原因:iter函数
- 解释器需要迭代对象x时,会自动调用iter(x)。
- 内置的 iter 函数有以下作用
- 1.检查对象是否实现了
__iter__
方法,如果实现了就调用它,获取一个迭代器。 - 2.如果没有实现
__iter__
方法,但是实现了__getitem__
方法,Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。 - 如果尝试失败,Python 抛出 TypeError 异常,通常会提示“x object is not iterable”
- 1.检查对象是否实现了
可迭代的对象与迭代器的对比
- 使用 iter 内置函数可以获取迭代器的对象。
如果对象实现了能返回迭代器的 __iter__ 方法,那么对象就是可迭代的。
- 所以
任何 Python 序列都可迭代的原因是,它们都实现了 __getitem__ 方法
,标准的序列也都实现了__iter__
方法。 - 从 Python 3.4 开始,检查对象 x 能否迭代,最准确的方法是:
调用 iter(x) 函数
,如果不可迭代,再处理 TypeError 异常 - 标准的迭代器接口有两个方法
-
__next__
:返回下一个可用的元素,如果没有元素了,抛出 StopIteration异常。 -
__iter__
:返回 self,以便在应该使用可迭代对象的地方使用迭代器,例如在 for 循环中。
-
- 下面使用前面的Sentence类来说明如何
使用 iter(...) 函数构建迭代器
,以及如何使用 next(...)函数使用迭代器
:
if __name__ == '__main__':
s = Sentence('hello world')
it = iter(s) # 构建迭代器
print(it)
print(next(it))
print(next(it))
print(next(it))
- output
<iterator object at 0x0BC82230>
hello
world
Traceback (most recent call last):
File "xxx.py", line 33, in <module>
print(next(it))
StopIteration
- 可知next方法会不断拿出迭代器中的元素,如果没有元素,返回StopIteration异常。如果使用next迭代完成后想再次迭代,必须重新构建迭代器,因为next会拿出迭代器中的元素而不放回:
if __name__ == '__main__':
s = Sentence('hello world')
it = iter(s) # 构建迭代器
print(it) # <iterator object at 0x0C242230>
print(next(it)) # hello
print(next(it)) # world
# print(next(it))
print(list(it)) # []
print(list(iter(s))) # 重新构建迭代器生成的列表并打印 # ['hello', 'world']
- 根据以上可以总结迭代器的定义:
实现了无参数的 __next__ 方法,返回序列中的下一个元素;如果没有元素了,那么抛出 StopIteration 异常。Python 中的迭代器还实现了 __iter__ 方法,因此迭代器也可以迭代。
- 下面根据Sentence类来实现标准的迭代器:
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 __iter__(self):
return SentenceIterator(self.words) # 返回迭代器
def __repr__(self):
return "Sentence({})".format(reprlib.repr(self.text))
class SentenceIterator:
"""迭代器类"""
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration
self.index += 1
return word
def __iter__(self):
return self
- 在 SentenceIterator 类中实现了
__iter__
方法看似没什么必要,不过必须这样做。因为迭代器应该实现__next__
和__iter__
两个方法。可迭代的对象有个 __iter__ 方法,每次都实例化一个新的迭代器;而迭代器要实现 __next__ 方法,返回单个元素,此外还要实现 __iter__ 方法,返回迭代器本身。
- 因此,
迭代器可以迭代,但是可迭代的对象不是迭代器。
- 各个迭代器要能维护自身的内部状态, 每次调用 iter(my_iterable) 都新建一个独立的迭代器。这就是为什么这个示例需要定义 SentenceIterator 类。由此,
可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现 __iter__ 方法,但不能实现 __next__ 方法。
- 你可能会想,难道我们定义一个可迭代的对象必须还得新增个迭代器对象吗,也就是说,我想要去掉SentenceIterator迭代器类,有没有更好的实现方式?这样,生成器就出来了。下面改写
__iter__
方法,使用生成器如下:
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 __iter__(self):
"""生成器函数"""
for word in self.words:
yield word # yield 可以简单理解为return
return
def __repr__(self):
return "Sentence({})".format(reprlib.repr(self.text))
- 这里的
__iter__
方法是生成器函数, 每次调用__iter__
方法都会自动创建迭代器,所以迭代器其实是生成器对象
。这里可能有点绕,下面来详细解释下生成器的原理。 - 生成器函数的工作原理:
只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器对象。
也就是说,生成器函数是生成器工厂。下面通过一个示例来说明生成器的行为。
def gen():
for i in range(3):
yield i
if __name__ == '__main__':
g = gen()
print(g)
for item in g:
print(item)
print(next(g))
- output
<generator object gen at 0x0C8ED300>
0
1
2
Traceback (most recent call last):
File "xxx.py", line 56, in <module>
print(next(g))
StopIteration
- gen函数在调用是返回一个生成器对象(generator ),这个生成器对象时迭代器,会生成传给 yield 关键字的表达式的值,由于这里的g是迭代器,所以迭代完成后使用next方法获取不到元素而报错,除非重新生成一个迭代器才可以进行迭代。
-
__iter__
方法是生成器函数,调用时会构建一个实现了迭代器接口的生成器对象,因此不用再定义额外的迭代器类了。 - 遍历列表会消耗不少内存,特别是在列表比较大时,而且如果我们只需要某几个元素,重复遍历列表显然有点杀鸡用牛刀。Sentence类中findall返回的是列表,能否直接返回迭代器呢?re.finditer就考虑到了这点,re.finditer返回的不是列表,而是一个生成器,按需生成 re.MatchObject 实例。这样
__iter__
无需遍历列表就可以直接获取生成器实例,显然能节省大量内存。如下示例:
class Sentence:
"""可迭代的对象"""
def __init__(self, text):
self.text = text
def __iter__(self):
for match in RE_WORD.finditer(self.text): # finditer 函数构建一个迭代器
yield match.group()
def __repr__(self):
return "Sentence({})".format(reprlib.repr(self.text))
- 想必python程序员经常会用到列表推导式,但是生成器表达式可能不太常用,生成器表达式构建一个生成器,但是会大大简化代码。下面通过一个例子先来看看列表推导式和生成器表达式的区别:
def gen():
for i in range(3):
print(i)
yield str(i)
if __name__ == '__main__':
res1 = [x*3 for x in gen()] # 列表推导式
print('---------------------------')
res2 = (x*3 for x in gen()) # 生成器表达式
for item in res1:
print(item)
print('---------------------------')
for item in res2:
print(item)
- output
0
1
2
---------------------------
000
111
222
---------------------------
0
000
1
111
2
222
- 可以看出,列表推导式会直接迭代完迭代器,返回一个列表,而
生成器表达式返回一个生成器对象
(res2),只有迭代这个生成器(res2)时才会执行迭代器函数。for 循环迭代 res2 时,实际上每次迭代时会隐式调用 next(res2),前进到 gen 函数中的下一个 yield 语句。 - 由于生成器表达式返回一个生成器对象,所以可以进一步简化Sentence类:
class Sentence:
"""可迭代的对象"""
def __init__(self, text):
self.text = text
def __iter__(self):
return (match.group() for match in RE_WORD.finditer(self.text))
def __repr__(self):
"""使用生成器表达式返回生成器对象"""
return "Sentence({})".format(reprlib.repr(self.text))
- 生成器表达式是语法糖:完全可以替换成生成器函数,不过有时使用生成器表达式更便利。
- 那么何时使用生成器表达式呢?和列表推导式一样,如果生成器表达式要分成多行写,建议定义生成器函数,以便提高可读性。在比较简单的情况下,生成器表达式可以代替列表推导式使用,这样做不用立即返回列表从而大大减少内存,在遇到大文件时尤其有用。
- 不过,生成器函数灵活得多,可以使用多个语句实现复杂的逻辑,也可以作为协程使用(后面说明)。
如果一个类只是为了构建生成器而去实现__iter__ 方法,那还不如使用生成器函数。
yield from
- 如果生成器函数需要产出另一个生成器生成的值,传统的解决方法是使用嵌套的 for 循环:
def chain(*iterables):
for it in iterables:
for i in it:
yield i
if __name__ == '__main__':
a = "abc"
b = range(4)
print(list(chain(a, b))) # ['a', 'b', 'c', 0, 1, 2, 3]
python3.3引入了yield from,使用yield from可以改进 chain 生成器函数:
def chain(*iterables):
for it in iterables:
yield from it
- 可以看出,yield from 完全代替了内层的 for 循环。
除了代替循环之外,yield from 还会创建通道,把内层生成器直接与外层生成器的客户端联系起来。
把生成器当成协程使用时,这个通道特别重要,不仅能为客户端代码生成值,还能使用客户端代码提供的值。
深入iter
- 在 Python 中迭代对象 x 时会调用 iter(x)。可是,iter 函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调用的对象创建迭代器。第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符。看下面的例子就知道了:
def d1():
return randint(1, 6)
if __name__ == '__main__':
a = iter(d1, 1)
for i in a:
print(i)
- 无论怎样运行,都不打印1,当可调用的对象返回为1时,和第二个参数(哨符)相同,就会触发StopIteration 异常,不产出哨符;也就是说,当随机数产出数字为哨符1 时,for循环终止。
- 这样的思想有很大用处,比如定时任务的终止回调、特定匹配回调等场景。