python3 cookbook 第四章
- 前言
- 手动遍历迭代器
- 代理迭代
- 使用生成器创建新的迭代模式
- 实现迭代器协议
- 反向迭代
- 带有外部状态的生成器函数
- 迭代器切片
- 跳过可迭代对象的开始部分
- 排列组合的迭代
- 序列上索引值迭代
- 同时迭代多个序列
- 不同集合上元素的迭代
- 创建数据处理管道
- 展开嵌套的序列
- 顺序迭代合并后的排序迭代对象
- 迭代器代替while无限循环
前言
这章节讲的是迭代器和生成器,学习之前可以看一下这个回答,先复习下可迭代对象,迭代器和生成器的相关知识。
手动遍历迭代器
这一小节的问题是,你想遍历一个可迭代对象中的所有元素,但是却不想使用for循环,但是举例子用的是读取文件,我他喵直接手动问号,这不是个迭代器吗,可迭代对象能直接next?
from collections import Iterable, Iterator
with open('format.txt', 'r', encoding='utf-8') as f:
print(isinstance(f, Iterator))
print(isinstance(f, Iterable))
# True
# True
书上的例子:
with open('/etc/passwd') as f:
while True:
line = next(f, None)
if line is None:
break
print(line, end='')
看上去远没有for循环清晰:
with open('/etc/passwd') as f:
for line in iter(lambda :f.readline(), ''):
print(line, 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):
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)
Python的迭代器协议需要 __iter__()
方法返回一个实现了 __next__()
方法的迭代器对象。 如果你只是迭代遍历其他容器的内容,你无须担心底层是怎样实现的。你所要做的只是传递迭代请求既可。
使用生成器创建新的迭代模式
简单的说就是生成器函数:
def frange(start, stop, increment):
x = start
while x < stop:
yield x
x += increment
一个生成器函数主要特征是它只会回应在迭代中使用到的 next 操作。 一旦生成器函数返回退出,迭代终止。我们在迭代中通常使用的for语句会自动处理这些细节,所以你无需担心。
实现迭代器协议
Python的迭代协议要求一个 __iter__()
方法返回一个特殊的迭代器对象, 这个迭代器对象实现了 __next__()
方法并通过 StopIteration
异常标识迭代的完成。
目前为止,在一个对象上实现迭代最简单的方式是使用一个生成器函数。在这段代码中,depth_first()
方法简单直观。 它首先返回自己本身并迭代每一个子节点并 通过调用子节点的 depth_first()
方法(使用 yield from
语句)返回对应元素。
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()
# 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)
反向迭代
很多程序员并不知道可以通过在自定义类上实现 __reversed__()
方法来实现反向迭代。比如:
class Countdown:
def __init__(self, start):
self.start = start
# Forward iterator
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
# Reverse iterator
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)
定义一个反向迭代器可以使得代码非常的高效, 因为它不再需要将数据填充到一个列表中然后再去reversed()
反向迭代这个列表。
带有外部状态的生成器函数
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='')
迭代器切片
def count(n):
while True:
yield n
n += 1
from itertools import islice
c = count(0)
for i in islice(c, 2, 8):
print(i)
print(next(c))
# 2 3 4 5 6 7
# 8
迭代器和生成器不能使用标准的切片操作,因为它们的长度事先我们并不知道(并且也没有实现索引)。 函数 islice()
返回一个可以生成指定元素的迭代器,它通过遍历并丢弃直到切片开始索引位置的所有元素。 然后才开始一个个的返回元素,并直到切片结束索引位置。
这里要着重强调的一点是 islice()
会消耗掉传入的迭代器中的数据。 必须考虑到迭代器是不可逆的这个事实。 所以如果你需要之后再次访问这个迭代器的话,那你就得先将它里面的数据放入一个列表中。
跳过可迭代对象的开始部分
def count(n):
while True:
if n>10:
raise StopIteration
yield n
n += 1
from itertools import dropwhile
c = count(0)
for i in dropwhile(lambda x:x<4, c):
print(i, end=' ')
# 4 5 6 7 8 9 10
首先介绍的是 itertools.dropwhile()
函数。使用时,你给它传递一个函数对象和一个可迭代对象。 它会返回一个迭代器对象,丢弃原有序列中直到函数返回Flase之前的所有元素,然后返回后面所有元素,就不是传统意义上的过滤,因为只会过滤掉开始部分满足测试条件的元素,后面的元素是不管的。书里的例子是跳过开头部分的注释行,感觉蛮好的。
with open('/etc/passwd') as f:
for line in dropwhile(lambda line: not line.startswith('#'), f):
print(line, end='')
排列组合的迭代
itertools
模块提供了三个函数来解决这类问题。 其中一个是 itertools.permutations()
, 它接受一个集合并产生一个元组序列,每个元组由集合中所有元素的一个可能排列组成。 也就是说通过打乱集合中元素排列顺序生成一个元组,比如:
>>> 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.combinations()
可得到输入集合中元素的所有的组合。比如:
>>> from itertools import combinations
>>> for c in combinations(items, 3):
... print(c)
...
('a', 'b', 'c')
>>> for c in combinations(items, 2):
... print(c)
...
('a', 'b')
('a', 'c')
('b', 'c')
>>> for c in combinations(items, 1):
... print(c)
...
('a',)
('b',)
('c',)
对于 combinations()
来讲,元素的顺序已经不重要了。 也就是说,组合 ('a', 'b')
跟 ('b', 'a')
其实是一样的(最终只会输出其中一个)。
在计算组合的时候,一旦元素被选取就会从候选中剔除掉(比如如果元素’a’已经被选取了,那么接下来就不会再考虑它了)。 而函数 itertools.combinations_with_replacement()
允许同一个元素被选择多次,比如:
>>> for c in combinations_with_replacement(items, 3):
... print(c)
...
('a', 'a', 'a')
('a', 'a', 'b')
('a', 'a', 'c')
('a', 'b', 'b')
('a', 'b', 'c')
('a', 'c', 'c')
('b', 'b', 'b')
('b', 'b', 'c')
('b', 'c', 'c')
('c', 'c', 'c')
序列上索引值迭代
内置函数enumerate()
,传入一个iterable
和开始参数。在for循环里想额外定义一个计数变量的时候,使用这个函数会更加方便。
word_summary = defaultdict(list)
with open('myfile.txt', 'r') as f:
lines = f.readlines()
for idx, line in enumerate(lines):
# Create a list of words in current line
words = [w.strip().lower() for w in line.split()]
for word in words:
word_summary[word].append(idx)
同时迭代多个序列
zip()
和itertools.zip_longest()
都可,都返回一个迭代器,不同的是前者的迭代长度跟参数中最短序列长度一致,后者是最长,缺失值默认用None
代替,也可以用关键字参数fillvalue
来指定。
>>> a = [1, 2, 3]`在这里插入代码片`
>>> b = ['w', 'x', 'y', 'z']
>>> for i in zip(a,b):
... print(i)
...
(1, 'w')
(2, 'x')
(3, 'y')
-----------------
>>> for i in zip_longest(a, b, fillvalue=0):
... print(i)
...
(1, 'w')
(2, 'x')
(3, 'y')
(0, 'z')
不同集合上元素的迭代
你想在多个对象执行相同的操作,但是这些对象在不同的容器中,你希望代码在不失可读性的情况下避免写重复的循环。itertools.chain()
方法可以用来简化这个任务。 它接受一个可迭代对象列表作为输入,并返回一个迭代器,有效的屏蔽掉在多个容器中迭代细节。 为了演示清楚,考虑下面这个例子:
>>> from itertools import chain
>>> a = [1, 2, 3, 4]
>>> b = ['x', 'y', 'z']
>>> for x in chain(a, b):
... print(x)
...
1
2
3
4
x
y
z
itertools.chain()
接受一个或多个可迭代对象作为输入参数。 然后创建一个迭代器,依次连续的返回每个可迭代对象中的元素。 这种方式要比先将序列合并再迭代要高效的多。比如:
# Inefficent
for x in a + b:
...
# Better
for x in chain(a, b):
...
第一种方案中, a + b
操作会创建一个全新的序列并要求a和b的类型一致。 chian()
不会有这一步,所以如果输入序列非常大的时候会很省内存。 并且当可迭代对象类型不一样的时候 chain()
同样可以很好的工作。
创建数据处理管道
为了处理文件,你可以定义一个由多个执行特定任务独立任务的简单生成器函数组成的容器。就像这样:
import os
import fnmatch
import gzip
import bz2
import re
def gen_find(filepat, top):
'''
Find all filenames in a directory tree that match a shell wildcard pattern
'''
for path, dirlist, filelist in os.walk(top):
for name in fnmatch.filter(filelist, filepat):
yield os.path.join(path,name)
def gen_opener(filenames):
'''
Open a sequence of filenames one at a time producing a file object.
The file is closed immediately when proceeding to the next iteration.
'''
for filename in filenames:
if filename.endswith('.gz'):
f = gzip.open(filename, 'rt')
elif filename.endswith('.bz2'):
f = bz2.open(filename, 'rt')
else:
f = open(filename, 'rt')
yield f
f.close()
def gen_concatenate(iterators):
'''
Chain a sequence of iterators together into a single sequence.
'''
for it in iterators:
yield from it
def gen_grep(pattern, lines):
'''
Look for a regex pattern in a sequence of lines
'''
pat = re.compile(pattern)
for line in lines:
if pat.search(line):
yield line
现在你可以很容易的将这些函数连起来创建一个处理管道。 比如,为了查找包含单词python的所有日志行,你可以这样做:
lognames = gen_find('access-log*', 'www')
files = gen_opener(lognames)
lines = gen_concatenate(files)
pylines = gen_grep('(?i)python', lines)
for line in pylines:
print(line)
如果将来的时候你想扩展管道,你甚至可以在生成器表达式中包装数据。 比如,下面这个版本计算出传输的字节数并计算其总和。
lognames = gen_find('access-log*', 'www')
files = gen_opener(lognames)
lines = gen_concatenate(files)
pylines = gen_grep('(?i)python', lines)
bytecolumn = (line.rsplit(None,1)[1] for line in pylines)
bytes = (int(x) for x in bytecolumn if x != '-')
print('Total', sum(bytes))
以管道方式处理数据可以用来解决各类其他问题,包括解析,读取实时数据,定时轮询等。
为了理解上述代码,重点是要明白 yield
语句作为数据的生产者而 for
循环语句作为数据的消费者。 当这些生成器被连在一起后,每个 yield
会将一个单独的数据元素传递给迭代处理管道的下一阶段。 在例子最后部分, sum()
函数是最终的程序驱动者,每次从生成器管道中提取出一个元素。
这种方式一个非常好的特点是每个生成器函数很小并且都是独立的。这样的话就很容易编写和维护它们了。 很多时候,这些函数如果比较通用的话可以在其他场景重复使用。 并且最终将这些组件组合起来的代码看上去非常简单,也很容易理解。
使用这种方式的内存效率也不得不提。上述代码即便是在一个超大型文件目录中也能工作的很好。 事实上,由于使用了迭代方式处理,代码运行过程中只需要很小很小的内存。
展开嵌套的序列
可以写一个包含 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)
顺序迭代合并后的排序迭代对象
heapq.merge
可迭代特性意味着它不会立马读取所有序列。 这就意味着你可以在非常长的序列中使用它,而不会有太大的开销。 比如,下面是一个例子来演示如何合并两个排序文件:
with open('sorted_file_1', 'rt') as file1, \
open('sorted_file_2', 'rt') as file2, \
open('merged_file', 'wt') as outf:
for line in heapq.merge(file1, file2):
outf.write(line)
有一点要强调的是 heapq.merge()
需要所有输入序列必须是排过序的。 特别的,它并不会预先读取所有数据到堆栈中或者预先排序,也不会对输入做任何的排序检测。 它仅仅是检查所有序列的开始部分并返回最小的那个,这个过程一直会持续直到所有输入序列中的元素都被遍历完。
迭代器代替while无限循环
iter
函数一个鲜为人知的特性是它接受一个可选的 callable
对象和一个标记(结尾)值作为输入参数。 当以这种方式使用的时候,它会创建一个迭代器, 这个迭代器会不断调用 callable
对象直到返回值和标记值相等为止。
这种特殊的方法对于一些特定的会被重复调用的函数很有效果,比如涉及到I/O调用的函数。 举例来讲,如果你想从套接字或文件中以数据块的方式读取数据,通常你得要不断重复的执行 read()
或 recv()
, 并在后面紧跟一个文件结尾测试来决定是否终止。这节中的方案使用一个简单的 iter()
调用就可以将两者结合起来了。 其中 lambda
函数参数是为了创建一个无参的 callable
对象,并为recv
或 read()
方法提供了 size
参数。
>>> 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
...
>>>