迭代是数据处理的基石,扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项,这就是迭代器模式。
所有的生成器都是迭代器,因为生成器实现了迭代器的接口,迭代器用于从集合中取出元素,生成器用于凭空生成元素。
所有的序列都可以迭代:
序列可以迭代的原因:iter函数
解释器需要迭代对象x的时候,会自动调用iter(x)函数,这个iter函数有这些作用:
- 检查对象是否实现了__iter__方法,如果实现了就调用它,获取一个迭代器。
- 如果没有iter方法,但是实现了__getitem__方法,那么会创建一个迭代器,尝试按照顺序从0开始获取元素。
def __getitem__(self, index):
return self.words[index]
- 如果上面的方法都尝试失败,会抛出TypeError异常
所以这样的话,迭代对象之前显式地检查对象是否可迭代是没有必要的,毕竟尝试迭代不可迭代的对象的时候,python抛出的异常信息是非常明显的:TypeError: C object is not iterable
综上所述:
可迭代的对象定义:
如果对象实现了能返回迭代器的__iter__方法,那么该对象就是可迭代对象;序列都是可迭代对象;如果对象实现了__getitem__方法,并且参数是从0开始的,那么该对象也是可迭代的。
所以可迭代对象和迭代器之间的关系是:python从可迭代对象中获取迭代器
标准的迭代器接口有两个:
- next:
返回下一个可用元素,如果没有元素,抛出StopIteration异常 - iter:
返回self,用于在应该使用可迭代对象的地方使用迭代器,例如for循环。
这是迭代期间发生的基本细节
>>> items = [1, 2, 3]
>>> # Get the iterator
>>> it = iter(items) # Invokes items.__iter__()
>>> # Run the iterator
>>> next(it) # Invokes it.__next__()
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
迭代器的定义:
迭代器是这样的对象,实现了无参数的__next__方法,返回序列中的下一个元素,如果没有元素了,那么就抛出StopIteration异常。此外,迭代器中也实现了__iter__方法,所以迭代器也可以迭代。迭代器是不可逆的!!!
Python的迭代器协议需要__iter__方法返回一个实现了__next__方法的迭代器对象
重点概念:
可迭代的对象一定不能是自身的迭代器,所以,可迭代对象必须实现__iter__方法,但是不能实现__next__方法。
迭代器应该一直可以迭代,迭代器的__iter__应该返回它本身。
- 可迭代对象的__iter__方法生成它的迭代器:
def __iter__(self):
return Iterator(self.words)
- 迭代器的__iter__方法返回它本身:
def __iter__(self):
return self
如果我们有一个自定义容器对象,里面包含有列表元组等其他的迭代对象,我们想在这个自定义对象上面执行迭代操作该怎么办呢?
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)
# Example
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
# Outputs Node(1), Node(2)
for ch in root:
print(ch)
解决方法:定义一个__iter__()方法,将迭代操作代理到容器内部的对象上面去,这里的iter()函数只是使用简化代码,就跟len(s)跟使用s.len()方法是一样的。
生成器:
当你想实现一个新的迭代模式,跟普通的内置函数比如range(),reversed()不一样的时候,就需要使用一个生成器函数来定义它。
生成器的__iter__方法:
def __iter__(self):
for word in self.words:
yield word
return
只要python函数的定义体中含有yield关键字,那么这个函数就是生成器函数
但是这个生成器函数和普通函数不同的是生成器只能用于迭代操作!!
调用next(生成器函数)会获取yield生成的下一个元素,生成器函数的定义体执行完毕之后,生成器对象会抛出StopIteration异常
使用生成器创建新的迭代模式:
如果想实现一个新的迭代模式,可以使用一个生成器来定义它:
# 这是实现某个范围内浮点数的生成器
def frange(start, stop, increment):
x = start
while x < stop:
yield x
x += increment
为了使用这个函数,可以使用for循环来迭代它或者使用其他的界都一个可迭代对象的函数:
1. for n in frange(0, 4, 0.5):
... print(n)
2. list(frange(0, 1, 0.125))
下面用一个例子来描述生成器的工作机制:
>>> def countdown(n):
... print('Starting to count from', n)
... while n > 0:
... yield n
... n -= 1
... print('Done!')
>>> c = countdown(3) # 创建一个生成器
>>> c
<generator object countdown at 0x1006a0af0>
>>> next(c) # 运行到第一次yield,也就是3
Starting to count from 3
3
>>> # Run to the next yield
>>> next(c)
2
>>> # Run to next yield
>>> next(c)
1
>>> # Run to next yield (iteration stops)
>>> next(c)
Done!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
生成器表达式:
生成器表达式可以理解为列表推导的惰性版本,不会迫切构建列表,而是会返回一个生成器。
def gen_AB():
print('start')
yield 'A'
print('continue')
yield 'B'
print('end')
-------------------------------------------------
res1 = [x*3 for x in gen_ab()] # 这是列表推导迫切的迭代gen_AB()函数生成的生成器
start
continue
end
for i in res1:
print(i)
AAA
BBB
--------------------------------------------------
res2 = (x*3 for x in gen_ab()) # res2是一个生成器对象,只有for循环迭代res2时,gen_AB()函数的定义体才能真正执行
for i in res2:
print(i)
start
AAA
continue
BBB
end
构建能支持迭代协议的自定义对象
在一个对象上实现迭代最简单的方式是使用一个生成器函数,下面的例子是以深度优先的方式遍历树形节点的生成器:
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): # 该对象有__iter__属性,所以该对象是一个可迭代对象
return iter(self._children)
def depth_first(self): # 这里是一个生成器
yield self # 首先返回节点本身,再返回它的子节点
for c in self:
yield from c.depth_first()
# Example
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)
# Outputs Node(0), Node(1), Node(3), Node(4), Node(2), Node(5)
python的迭代协议要求__iter__能返回一个特殊的迭代器对象,这个迭代器对象实现了__next__方法,但是真的实现这样的东西会非常的复杂,因为迭代器必须在迭代过程中维护大量的状态信息,但是将迭代器定义为一个生成器后,问题就简单多了。
反向迭代:
首先可以使用内置的reversed()函数,但是反向迭代仅仅当对象的大小可预先确定或者对象实现了 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)
带有外部状态的生成器函数
目的是做一个生成器函数暴露外部状态给用户,方法是将它实现为一个类,然后把生成器函数放到__iter__方法中,这样不会改变任何的算法逻辑,由于这是类中的一个方法,所以可以提供各种方法和属性来供用户使用:
如果在做迭代操作的时候不使用for循环,那么需要先调用iter()函数来生成迭代器,然后再使用next()操作
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()
那么怎么使用这个类呢?
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='')
迭代器和生成器切片:
如果想得到一个迭代器生成的切片对象,但是标准的切片方法不能实现这个功能,我们就使用itertools.islice()来对迭代器和生成器做切片操作:
>>> def count(n):
... while True:
... yield n
... n += 1
...
>>> c = count(0)
>>> c[10:20] # 这里并不能使用普通的切片方法,因为迭代器的长度我们事先不知道
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable
>>> # Now using islice()
>>> import itertools
>>> for x in itertools.islice(c, 10, 20): # 这个函数返回一个可以生成指定元素的迭代器,通过遍历并丢弃直到切片开始索引位置。
... print(x)
10
...
19
>>>
跳过可迭代对象的部分内容
itertools.dropwhile()
函数可以传递一个函数对象和一个可迭代对象,它会返回一个迭代器对象,丢弃原有序列中直到函数返回Flase之前的所有元素,然后返回后面所有元素,所以就是丢弃前面所有返回True的元素,直到第一个False出现。
如果想跳过开始部分的注释行的话,可以使用下面的方法:
>>> from itertools import dropwhile
>>> with open('/etc/passwd') as f:
... for line in dropwhile(lambda line: line.startswith('#'), f):
... print(line, end='')
排列组合的迭代
如果你想遍历一个集合中元素的所有可能的排列组合,可以使用下面的方法:
>>> items = ['a', 'b', 'c']
>>> from itertools import permutations
>>> for p in permutations(items):
... print(p)
...
('a', 'b', 'c')
('a', 'c', 'b')
('b', 'a', 'c')
('b', 'c', 'a')
('c', 'a', 'b')
('c', 'b', 'a')
>>>
itertools.permutations()函数接受一个集合并产生一个元组序列,每个元组都是集合中所有元素的一个可能的排列组合。如果想对所有排列组合指定长度,可以再传递一个参数:for p in permutations(items, 2):
使用 itertools.combinations() 可得到输入集合中元素的所有的组合:
>>> for c in combinations(items, 2):
... print(c)
...
('a', 'b')
('a', 'c')
('b', 'c')
这个函数不区别对待元素的顺序,所以ab和ba是一样的,只会输出一个。
所以当我们遇到比较复杂的迭代问题的时候,我们都可以去itertools模块里面看一看!!!
迭代中得到元素索引值
如果在迭代过程中想跟踪正在被处理的元素的索引值,我们可以使用内置的enumerate()函数来处理
>>> my_list = ['a', 'b', 'c']
>>> for idx, val in enumerate(my_list):
... print(idx, val)
...
0 a
1 b
2 c
如果想从1开始计数,我们可以在my_list后面传递一个参数1。
enumerate()函数返回一个enumerate对象实例,这个对象实例是一个迭代器,回连续的包含一个计数和一个值的元组。
同时迭代多个序列
如果你想同事迭代两个序列,每次分别从每个序列中取出一个元素,那么我们可以使用zip()函数:
>>> xpts = [1, 5, 4, 2, 10, 7]
>>> ypts = [101, 78, 37, 15, 62, 99]
>>> for x, y in zip(xpts, ypts):
... print(x,y)
...
1 101
一旦其中某个序列到底结尾了,那么迭代就宣告结束,但是如果我们想以长序列来结束,我们可以使用itertools.zip_longest()
在处理成对数据的时候,zip()函数是非常有用的,比如你有两个列表,分别是键列表和值列表,那么我们可以这样:
headers = ['name', 'shares', 'price']
values = ['ACME', 100, 490.1]
s = dict(zip(headers,values))
展开有嵌套的序列–数组拍平
可以使用一个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)
迭代器代替while无限循环
在代码中使用while循环来迭代处理是因为它需要调用某个函数和其他迭代方式不同的测试条件。
首先展示一个常见的while循环:
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''):
# 这算是iter()一个鲜为人知的特性,可以选择一个callable对象和一个结尾标记作为参数。
# 它会生产一个迭代器,不断调用这个callable对象,直到返回值和这个结尾标记一样
pass
# process_data(data)