迭代是 Python 最强大的功能之一。初看起来,你可能会简单的认为迭代只不过是处理序列中元素的一种方法。然而,绝非仅仅就是如此,还有很多你可能不知道的,比如创建你自己的迭代器对象,在 itertools 模块中使用有用的迭代模式,构造生成器函数等等。
手动遍历迭代器:
>>> it = iter(range(2))
>>> next(it)
0
>>> next(it)
1
>>> next(it)
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
next(it)
StopIteration
>>>
当没有值的时候,我们再次调用next,则会产生错误。所以为了手动的遍历可迭代对象,使用 next() 函数并在代码中捕获 StopIteration 异常。
>>> def it(num):
r = iter(range(num))
while True:
try:
print(next(r))
except StopIteration:
break
>>>
>>> it(10)
0
1
2
3
4
5
6
7
8
9
>>>
通常来讲,StopIteration 用来指示迭代的结尾。然而,如果你手动使用上面演示的 next() 函数的话,你还可以通过返回一个指定值来标记结尾,比如 None 。下面是示例:
>>> def it(num):
r = iter(range(num))
while True:
x = next(r,None)
if x is None:
break
print(x)
>>> it(3)
0
1
2
>>>
大多数情况下,我们会使用 for 循环语句用来遍历一个可迭代对象。但是,偶尔也需要对迭代做更加精确的控制,这时候了解底层迭代机制就显得尤为重要了。
自定义构建一个可迭代对象:
你构建了一个自定义容器对象,里面包含有列表、元组或其他可迭代对象。你想直接在你的这个新容器对象上执行迭代操作。
class Node:
def __init__(self, value):
self._value = value
self._children = []
def __repr__(self):
return 'Node({!r})'.format(self._value)
def add_child(self, node):
self._children.append(node)
def __iter__(self):
return iter(self._children)
>>> n1 = Node(0)
>>> n2 = Node(2)
>>> n3 = Node(3)
>>> n1.add_child(n2)
>>> n1.add_child(n3)
>>> n1
Node(0)
>>> list(n1)
[Node(2), Node(3)]
>>> for i in n1:
print(i)
Node(2)
Node(3)
>>>
在上面代码中,iter() 方法只是简单的将迭代请求传递给内部的 _children
属性。
Python 的迭代器协议需要 iter() 方法返回一个实现了 next() 方法的迭代器对象。如果你只是迭代遍历其他容器的内容,你无须担心底层是怎样实现的。你所要做的只是传递迭代请求既可。这里的 iter() 函数的使用简化了代码,iter(s) 只是简单的通过调用 s.iter() 方法来返回对应的迭代器对象,就跟 len(s) 会调用 s.len() 原理是一样的。
你想实现一个自定义迭代模式,跟普通的内置函数比如 range() , reversed() 不一样。
>>> def func(start,stop,sep):
x = start
while x < stop:
yield x
x += sep
>>> for i in func(1,3,0.5):
print(i)
1
1.5
2.0
2.5
>>>
一个函数中需要有一个 yield 语句即可将其转换为一个生成器。跟普通函数不同的是,生成器只能用于迭代操作。
>>> f = func(0,2,0.5)
>>> f
<generator object func at 0x7fe54e03fc78>
>>> next(f)
0
>>> next(f)
0.5
>>> next(f)
1.0
>>> next(f)
1.5
>>> next(f)
Traceback (most recent call last):
File "<pyshell#56>", line 1, in <module>
next(f)
StopIteration
>>>
一个以深度优先方式遍历树形节点的生成器。
class Node:
def __init__(self, value):
self._value = value
self._children = []
def __repr__(self):
return 'Node({!r})'.format(self._value)
def add_child(self, node):
self._children.append(node)
def __iter__(self):
return iter(self._children)
def depth_first(self):#这里是重点,递归返回节点
yield self
for c in self:
yield from c.depth_first()
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
child1.add_child(Node(3))
child1.add_child(Node(4))
child2.add_child(Node(5))
for ch in root.depth_first():
print(ch)
在这段代码中,depth_first() 方法简单直观。它首先返回自己本身并迭代每一个子节点并通过调用子节点的 depth_first() 方法 (使用 yield from 语句) 返回对应元素。
你想反方向迭代一个序列
>>> a = '1234'
>>> for i in reversed(a):
print(i)
4
3
2
1
反向迭代仅仅当对象的大小可预先确定或者对象实现了 reversed() 的特殊
方法时才能生效。如果两者都不符合,那你必须先将对象转换为一个列表才行.
如果可迭代对象元素很多的话,将其预先转换为一个列表要消耗大量的内存。
自定义类上实现 reversed() 方法来实现反向迭代
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
def __reversed__(self):
n = 1
while n <= self.start:
yield n
n += 1
for rr in reversed(Countdown(30)):
print(rr)
for rr in Countdown(30):
print(rr)
定义一个反向迭代器可以使得代码非常的高效,因为它不再需要将数据填充到一个列表中然后再去反向迭代这个列表。
迭代器切片
函数 itertools.islice() 正好适用于在迭代器和生成器上做切片操作。
>>> def count(n):
while True:
yield n
n += 1
>>> c = count(0)
>>> c[2:20]
Traceback (most recent call last):
File "<pyshell#66>", line 1, in <module>
c[2:20]
TypeError: 'generator' object is not subscriptable
>>> import itertools
>>> for x in itertools.islice(c,10,20):
print(x)
10
11
12
13
14
15
16
17
18
19
>>>
迭代器和生成器不能使用标准的切片操作,因为它们的长度事先我们并不知道 (并且也没有实现索引)。函数 islice() 返回一个可以生成指定元素的迭代器,它通过遍历并丢弃直到切片开始索引位置的所有元素。然后才开始一个个的返回元素,并直到切片结束索引位置。这里要着重强调的一点是 islice() 会消耗掉传入的迭代器中的数据。必须考虑到迭代器是不可逆的这个事实。所以如果你需要之后再次访问这个迭代器的话,那你就得先将它里面的数据放入一个列表中。
展开嵌套的序列
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) 用来将字符串和字节排除在可迭代对象外,防止将它们再展开成单个的字符。这样的话字符串数组就能最终返回我们所期望的结果了.
顺序迭代合并后的排序迭代对象
你有一系列排序序列,想将它们合并后得到一个排序序列并在上面迭代遍历。
>>> a = [1,7,8]
>>> b = [2,5,6,9,11]
>>> for c in heapq.merge(a,b):
print(c)
1
2
5
6
7
8
9
11
>>>
有一点要强调的是 heapq.merge() 需要所有输入序列必须是排过序的。特别的,它并不会预先读取所有数据到堆栈中或者预先排序,也不会对输入做任何的排序检测。它仅仅是检查所有序列的开始部分并返回最小的那个,这个过程一直会持续直到所有输入序列中的元素都被遍历完。
迭代器代替 while 无限循环
一个常见的 IO 操作程序可能会想下面这样:
CHUNKSIZE = 8192
def reader(s):
while True:
data = s.recv(CHUNKSIZE)
if data == b'':
break
process_data(data)
def reader2(s):
for chunk in iter(lambda: s.recv(CHUNKSIZE), b''):
pass
>>> import sys
>>> f = open('/etc/passwd')
>>> for chunk in iter(lambda: f.read(10), ''):
... n = sys.stdout.write(chunk)
...
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
daemon:*:1:1:System Services:/var/root:/usr/bin/false
_uucp:*:4:4:Unix to Unix Copy Protocol:/var/spool/uucp:/usr/sbin/uucico
iter 函数一个鲜为人知的特性是它接受一个可选的 callable 对象和一个标记 (结尾) 值作为输入参数。当以这种方式使用的时候,它会创建一个迭代器,这个迭代器会不断调用 callable 对象直到返回值和标记值相等为止。
这种特殊的方法对于一些特定的会被重复调用的函数很有效果,比如涉及到 I/O调用的函数。举例来讲,如果你想从套接字或文件中以数据块的方式读取数据,通常你得要不断重复的执行 read() 或 recv() ,并在后面紧跟一个文件结尾测试来决定是否终止。这节中的方案使用一个简单的 iter() 调用就可以将两者结合起来了。其中lambda 函数参数是为了创建一个无参的 callable 对象,并为 recv 或 read() 方法提供了 size 参数。
作为协程的前导,思考以下代码:
def simple(a):
print("程序开始a =",a)
b = yield a
print("接收到结果b =",b)
c = yield a + b
print("接收到结果c =",c)
>>> cor = simple(100)
>>> next(cor)
程序开始a = 100
100
>>> cor.send(20)
接收到结果b = 20
120
>>> cor.send(50)
接收到结果c = 50
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
cor.send(50)
StopIteration
>>>
思考yield的前后执行顺序,在哪里暂停,又从哪里开始。