迭代器
迭代器(iterator)就是一种可迭代对象。所谓的 迭代器就是重复做一件事,它又称为游标(cursor),它是程序设计的软件设计模式,是一种可在容器物件(container,如列表等)上实现元素遍历的接口。迭代器是一种特殊的数据结构,在 python 中,它也是以对象的形式存在的。
简单来说,在 python2 中存在 next 方法的可迭代对象是迭代器;而在 python3 中则变成了 next 方法。因此迭代器同时具有 iter 和 next 这两种方法。
通过 python 内置函数 iter 可以将一个可迭代对象转换成一个迭代器。为什么要将可迭代对象转换成迭代器呢?因为只有迭代器才能使用 python 内置函数 next。
迭代器会保存一个指针,指向可迭代对象的当前元素。调用 next 函数的时候,会返回当前元素,并将指针指向下一个元素。当没有下一个元素的时候,它会抛出 StopIteration 异常。
一个简单的迭代器用法:
lst = [[‘m’, 2, 4, 5], [‘x’, 3, 4, 5]]
for x in lst:
key = x[0]
for v in x[1:]:
print()
for x in lst:
it = iter(x)
key = next(it)
for v in it:
print()
使用第二种循环也就是迭代器会比第一种更有效率,因为切片将列表复制一份,占用的内存更多。
for 循环对于可迭代对象首先会调用 iter 方法将之转换为迭代器,然后不断的调用 next 方法,直到抛出 StopIteration 异常。
it = iter(itratable)
while True:
try:
next(it)
except StopIteration:
return
生成器
生成器也是函数,函数中只要有 yield 关键字,那么它就是生成器函数,返回值为生成器。生成器存在 iter 和 next 这两种方法,因此它是一个迭代器。生成器应用非常广泛,官方的异步 IO 基本上都是基于 yield 做的。当我们在 async def 定义的函数中使用 yield,那么这个函数就被称为异步生成器。
当我们调用生成器函数的时候,它会返回一个生成器对象,我们要使用一个变量去接受它,然后通过操作这个变量去操作生成器。生成器也是函数,函数都是从上到下执行,当执行到 yield 语句时,这个函数就停止了,并且会将此次的返回值返回。如果 yield 语句后没有任何值,那么它的返回值就是 None;如果有值,会将这个值返回给调用者。如果使用了生成器的 send 方法(下面会提到),那么返回值将是通过这个方法传递进去的值(前提是 yield 语句后没有任何值)。
所有的这些特性让生成器看起来和协程非常相似:可以多次调用、有多个切入点、执行可以被暂停。唯一的区别是生成器函数无法控制 yield 之后应继续执行的位置,因为控制权在调用者的手中。
对于一个没有调用结束的生成器,我们可以使用 close 方法将其关闭,可以将其写在 try 的 finally 语句中。
当使用 yield from 时,它将提供的表达式视为子迭代器,该子迭代器生成的所有值直接传递给当前生成器函数的调用者。任何传递给 send() 的值和通过throw() 传入的异常都会传递给基础迭代器(如果它有适当的方法去接收)。如果不是这种情况,那么 send() 会引发 AttributeError 或 TypeError,而 throw() 会立即引发传入的异常。
定义一个生成器:
def fn():
… for i in range(10):
… yield i
…fn() # 可以看到它是一个生成器
Out[3]: <generator object fn at 0x7f667fa5d0a0>f = fn() # 我们得先接收这个生成器
next(f) # 然后再对生成器进行操作
Out[6]: 0next(f)
Out[7]: 1
从函数的执行流程中可以知道,函数执行完毕之后现场应该被销毁,但是生成器却并不是这样。
执行流程剖析,先定义一个函数:
def g1():
… print(‘a’)
… yield 1
… print(‘b’)
… yield 2
… print(‘c’)
… return 3
…g = g1() # 没有输出 a,证明执行生成器函数的时候不会执行函数体
g # 可以看出是一个生成器,证明 return 没有生效
Out[10]: <generator object g1 at 0x7f667e6fa990>
通过 next 函数执行一把生成器:
next(g) # 执行到第一个 yield 后,停止执行
a
Out[11]: 1
再执行一次:
next(g) # 从第一个 yield 之后执行,到第二个 yield 停止
b
Out[12]: 2
继续执行:
next(g) # 从第二个 yield 之后执行,当没有更多 yield 之后,抛出异常,异常的值正好是函数的返回值
c # 下面的语句还是会执行的
Traceback (most recent call last):
File “/usr/local/python/lib/python3.6/site-packages/IPython/core/interactiveshell.py”, line 2862, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File “”, line 1, in
next(g)
StopIteration: 3
生成器函数的特点:
生成器函数执行的时候并不会执行函数体;
当 next 生成器的时候,会从当前代码执行到之后的第一个 yield,会弹出值并暂停函数;
当再次 next 生成器的时候,从上次暂停处开始向下执行;
当没有多余 yield 的时候,会抛出 StopIteration 异常,异常的 Value 是函数的返回值。
生成器是惰性求值的。比如我们可以定义一个计数器:
def make_inc():
def counter():
x = 0
while True:
x += 1
yield x
c = counter()
return lambda: next©
incr = make_inc()
incr()
Out[9]: 1incr()
Out[10]: 2
求斐波那契数列第 11 项:
def fib():
a = 0
b = 1
while True:
a, b = b, a+b
yield a
f = fib()
for _ in range(10):
… next(f)
…print(next(f))
89
可以看到递归都可以通过生成器来解决,并且没有递归深度的限制,也没有递归慢的缺点,因为它不需要保存现场。
以上都只是生成器的普通用法,协程才是生成器的高级用法。
进程和线程的调度是通过操作系统完成的,但是协程的调度是由用户态,也就是用户进行的。一旦函数执行到 yield 之后,它会暂停,暂停也就意味着让出 cpu 了。那么接下来就由用户决定执行什么代码。
当我们要对一个可迭代对象的前一项或几项做特殊处理时,如果直接对其进行循环的话,我们还需要判断是不是其第一个元素,或许我们还要在其外部定义一个计数器,这其实是一种和古老和 low 的方式。有了生成器之后,我们就可以在循环之前使用 next() 函数取出其中的第一个值,然后再对其进行 for 循环即可。如果无法对其直接使用 next 方法,那就调用它的 iter() 方法将其变成一个生成器后再继续。
yield
函数中一旦使用了 yield,这个函数就变成了生成器函数。但 yield 不能和 return 共存,并且 yield 只能定义在函数中。当我们调用这个函数的时候,函数内部的代码并不立即执行,这个函数只是返回一个生成器对象。当我们使用 for 对其进行迭代的时候,函数内的代码才会被执行。
python3 新增了 yield from 语法,它相当于 for + yield。比如:
yield from a()
等同于下面
for i in a():
yield i
yield 和 return 的区别:
return 的时候这个函数的局部变量都被销毁了;
所有 return 是得到所有结果之后的返回;
yield 是产生了一个可以恢复的函数(生成器),恢复了局部变量;
生成器只有在调用 .next() 时才运行函数生成一个结果。
yield 会记住函数执行的位置,下次再次执行时会从上次的位置继续向下执行。而如果在函数中使用 return,函数就直接退出了,无法继续执行。定义一个生成器:
def fun1(n):
… for i in xrange(n):
… yield i
…
先执行一下:
a = fun1(5)
a.next()
0
然后再对其进行循环会从之前的地方继续向下:
for i in a:print i
…
1
2
3
4
yield 的用处在于如果函数每次循环都会产生一个字串,如果想要将这些字串都传递给函数外的其他变量使用 return 是不行的,因为当函数第一次循环时碰到 return 语句整个函数就退出了,是不可能继续循环的,也就是说只能传递一个字串出去。这显然不符合我们的要求,这时就可以通过 yield 搞定了。
实现xrange:
def xrange(n):
start = 0
while True:
if start >= n:
return
yield start
start += 1
具体案例:
import csv
from pyzabbix import ZabbixAPI
zapi = ZabbixAPI(‘http://127.0.0.1/api_jsonrpc.php’)
zapi.login(‘uxeadmin’, ‘Uxe(00456)AdmIN.^??’)
with open(’_zabbix.csv’, ‘w’, encoding=‘gbk’) as f:
spamwriter = csv.writer(f)
for i in zapi.host.get(output=[“host”]):
item_info = zapi.item.get(hostids=i[‘hostid’], output=[“name”, ‘status’]).iter()
for j in item_info:
if not int(j[‘status’]):
spamwriter.writerow([i[‘host’], j[‘name’]])
break
for j in item_info:
if not int(j[‘status’]):
spamwriter.writerow([’’, j[‘name’]])
生成器方法
请注意,在生成器已经执行时调用下面的任何生成器方法会引发 ValueError 异常。
next
开始执行一个生成器或者从上一次 yield 语句后继续执行。当使用该方法继续(注意是继续而不是第一次执行)时,那么当前 yield 的返回值为 None,直到执行到下一次的 yield 语句时,yield 语句后的表达式的结果才会返回给调用者。当迭代器结束时会抛出 StopIteration 异常。
该方法会被 for 以及内置函数 next 隐式的调用。
send
继续执行生成器(注意是继续而不是第一次执行),并发送一个值到生成器函数。send 方法的参数是下一个 yield 语句的返回值,前提是 yield 语句中要事先接收它传递的参数。如果使用该方法启动(也就是第一次执行)生成器,必须使用 None 作为其参数,因为此时还没有 yield 能够接收它的值(毕竟接收该值的语句还没有开始执行)。
def fn():
a = 0
while True:
a += 1
r = yield # r 就是接收 send 参数的变量
print(’{} => {}’.format(a, r))
f = fn()
f.send(‘a’) # 不传递 None 的后果
Traceback (most recent call last):
File “/opt/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py”, line 2910, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File “”, line 1, in
f.send(‘a’)
TypeError: can’t send non-None value to a just-started generatornext(f) # 也可以不传递 None 而是使用 next 执行,两种方式都可以
f.send(‘a’)
1 => af.send(‘b’)
2 => b
throw
用法:
throw(type[, value[, traceback]])
传递一个 type 类型的异常给生成器,在生成器暂停的时候抛出,并且返回下一次 yield 的值。
close
在生成器函数暂停的位置引发 GeneratorExit。如果生成器函数正常退出,已经关闭,或者引发 GeneratorExit(没有捕获该异常),关闭返回给调用者;如果生成器产生一个值,则引发一个 RuntimeError;如果生成器引发其他异常,则传播给调用者;如果生成器由于异常或正常退出而退出,则 close() 不执行任何操作。
示例
def echo(value=None):
… print(“Execution starts when ‘next()’ is called for the first time.”)
… try:
… while True:
… try:
… value = (yield value) # 不管 yield 后面是否有表达式,value 的值都是 send 传递进来的参数
… except Exception as e:
… value = e
… finally:
… print(“Don’t forget to clean up when ‘close()’ is called.”)
…generator = echo(1)
print(next(generator))
Execution starts when ‘next()’ is called for the first time.
1print(next(generator))
Noneprint(generator.send(2))
2generator.throw(TypeError, “spam”)
TypeError(‘spam’,)generator.close()
Don’t forget to clean up when ‘close()’ is called.
生成器解析
python3 中的 range 函数就是一个典型的生成器,无论给它一个多么大的数,它占用内存始终很小。但是下面的代码会返回一个占用空间很大的列表:
[x ** 2 for x in range(100000)]
当我们想让它返回的结果也像生成器一样可以将中括号换成小括号:
(x ** 2 for x in range(100000))
<generator object at 0x7fb246656620>
使用 next 函数就可以查看里面的每个值,当然 for 循环也可以。
因此将列表解析的中括号变成小括号就是生成器的语法。
生成器解析其实就是列表解析的扩展,当我们明确需要使用小标访问的时候,使用列表解析。而如果只需要对结果进行迭代的时候,优先使用生成器解析。
还有一个场景,就是要对结果进行缓存的时候,就只能使用列表解析了。不过使用生成器解析的场景确实要比列表解析来的多。
暴露生成器内的对象
如果你想让你的生成器暴露外部状态给用户, 别忘了你可以简单的将它实现为一个类,然后把生成器函数放到 iter() 方法中过去。比如:
from collections import deque
class linehistory:
def init(self, lines, histlen=3):
self.lines = lines
self.history = deque(maxlen=histlen)
def __iter__(self):
for lineno, line in enumerate(self.lines, 1):
self.history.append((lineno, line))
yield line
def clear(self):
self.history.clear()
为了使用这个类,你可以将它当做是一个普通的生成器函数。然而,由于可以创建一个实例对象,于是你可以访问内部属性值,比如 history 属性或者是 clear() 方法。代码示例如下:
with open(‘somefile.txt’) as f:
lines = linehistory(f)
for line in lines:
if ‘python’ in line:
for lineno, hline in lines.history:
print(’{}:{}’.format(lineno, hline), end=’’)
如果行中包含了 python 这个关键字,那就打印该行和前三行的行号以及内容。
关于生成器,很容易掉进函数无所不能的陷阱。如果生成器函数需要跟你的程序其他部分打交道的话(比如暴露属性值,允许通过方法调用来控制等等),可能会导致你的代码异常的复杂。如果是这种情况的话,可以考虑使用上面介绍的定义类的方式。在 iter() 方法中定义你的生成器不会改变你任何的算法逻辑。由于它是类的一部分,所以允许你定义各种属性和方法来供用户使用。
一个需要注意的小地方是,如果你在迭代操作时不使用 for 循环语句,那么你得先调用 iter() 函数。比如:
f = open(‘somefile.txt’)
lines = linehistory(f)
next(lines)
Traceback (most recent call last):
File “”, line 1, in
TypeError: ‘linehistory’ object is not an iterator
Call iter() first, then start iterating
it = iter(lines)
next(it)
‘hello world\n’next(it)
‘this is a test\n’
生成器切片
你想得到一个由迭代器生成的切片对象,但是标准切片操作并不能做到。函数 itertools.islice() 正好适用于在迭代器和生成器上做切片操作。比如:
def count(n):
… while True:
… yield n
… n += 1
…c = count(0)
c[10:20]
Traceback (most recent call last):
File “”, line 1, in
TypeError: ‘generator’ object is not subscriptable
Now using islice()
import itertools
for x in itertools.islice(c, 10, 20):
… print(x)
…
10
11
12
13
14
15
16
17
18
19
迭代器和生成器不能使用标准的切片操作,因为它们的长度事先我们并不知道(并且也没有实现索引)。函数 islice() 返回一个可以生成指定元素的迭代器,它通过遍历并丢弃直到切片开始索引位置的所有元素。然后才开始一个个的返回元素,并直到切片结束索引位置。
这里要着重强调的一点是 islice() 会消耗掉传入的迭代器中的数据。必须考虑到迭代器是不可逆的这个事实。所以如果你需要之后再次访问这个迭代器的话,那你就得先将它里面的数据放入一个列表中。
跳过可迭代对象开始部分
你想遍历一个可迭代对象,但是它开始的某些元素你并不感兴趣,想跳过它们。itertools 模块中有一些函数可以完成这个任务。首先介绍的是 itertools.dropwhile() 函数。使用时,你给它传递一个函数对象和一个可迭代对象。它会返回一个迭代器对象,丢弃原有序列中直到函数返回 Flase 之前的所有元素,然后返回后面所有元素。
为了演示,假定你在读取一个开始部分是几行注释的源文件。比如:
with open(’/etc/passwd’) as f:
… for line in f:
… print(line, end=’’)
…
User Database
Note that this file is consulted directly only when the system is running
in single-user mode. At other times, this information is provided by
Open Directory.
…
nobody::-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root::0:0:System Administrator:/var/root:/bin/sh
…
如果你想跳过开始部分的注释行的话,可以这样做:
from itertools import dropwhile
with open(’/etc/passwd’) as f:
… for line in dropwhile(lambda line: line.startswith(’#’), f):
… print(line, end=’’)
…
nobody::-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root::0:0:System Administrator:/var/root:/bin/sh
…
这个例子是基于根据某个测试函数跳过开始的元素。如果你已经明确知道了要跳过的元素的个数的话,那么可以使用 itertools.islice() 来代替。比如:
from itertools import islice
items = [‘a’, ‘b’, ‘c’, 1, 4, 10, 15]
for x in islice(items, 3, None):
… print(x)
…
1
4
10
15
在这个例子中,islice() 函数最后那个 None 参数指定了你要获取从第 3 个到最后的所有元素。如果 None 和 3 的位置对调,意思就是仅仅获取前三个元素,这个跟切片的相反操作 [3:] 和 [:3] 原理是一样的。
函数 dropwhile() 和 islice() 其实就是两个帮助函数,为的就是避免写出下面这种冗余代码:
with open(’/etc/passwd’) as f:
# Skip over initial comments
while True:
line = next(f, ‘’)
if not line.startswith(’#’):
break
# Process remaining lines
while line:
# Replace with useful processing
print(line, end='')
line = next(f, None)
跳过一个可迭代对象的开始部分跟通常的过滤是不同的。比如,上述代码的第一个部分可能会这样重写:
with open(’/etc/passwd’) as f:
lines = (line for line in f if not line.startswith(’#’))
for line in lines:
print(line, end=’’)
这样写确实可以跳过开始部分的注释行,但是同样也会跳过文件中其他所有的注释行。换句话讲,我们的解决方案是仅仅跳过开始部分满足测试条件的行,在那以后,所有的元素不再进行测试和过滤了。
最后需要着重强调的一点是,本节的方案适用于所有可迭代对象,包括那些事先不能确定大小的,比如生成器,文件及其类似的对象。
展开嵌套的序列
你想将一个多层嵌套的序列展开成一个单层列表,可以写一个包含 yield from 语句的递归生成器来轻松解决这个问题。比如:
from collections import Iterable
def flatten(items, ignore_types=(str, bytes)):
for x in items:
if isinstance(x, Iterable) and not isinstance(x, ignore_types):
yield from flatten(x)
else:
yield x
items = [1, 2, [3, 4, [5, 6], 7], 8]
Produces 1 2 3 4 5 6 7 8
for x in flatten(items):
print(x)
在上面代码中,isinstance(x, Iterable) 检查某个元素是否是可迭代的。如果是的话,yield from 就会返回所有子例程的值。最终返回结果就是一个没有嵌套的简单序列了。
额外的参数 ignore_types 和检测语句 isinstance(x, ignore_types) 用来将字符串和字节排除在可迭代对象外,防止将它们再展开成单个的字符。 这样的话字符串数组就能最终返回我们所期望的结果了。比如:
items = [‘Dave’, ‘Paula’, [‘Thomas’, ‘Lewis’]]
for x in flatten(items):
… print(x)
…
Dave
Paula
Thomas
Lewis
之前提到的对于字符串和字节的额外检查是为了防止将它们再展开成单个字符。如果还有其他你不想展开的类型,修改参数 ignore_types 即可。
最后要注意的一点是,yield from 在涉及到基于协程和生成器的并发编程中扮演着更加重要的角色。