Python Cookbook第三版学习笔记【第四章】

第四章 迭代器和生成器

4.1 手动访问迭代器中的元素

  • 使用next()函数,当迭代器迭代结束时会抛出StopIteration异常,手动对异常进行捕获
a = [1,2,3]
b = iter(a)
try:
	while True:
		print(next(b))
except StopIteration:
	pass
  • 也可以给next()函数传入一个结束值,在迭代结束时返回这个值,就可以换成这种写法:
while True:
    curr_val = next(b, None)
    if curr_val is None:
        break
    print(curr_val)

4.2 委托迭代

  • 如果我们构建了一个自定义的容器对象,想要对这个容器进行迭代(即使之成为可迭代对象),可以实现__iter__()方法,返回其内部持有的一个迭代器:
class Container:
    def __init__(self, a: Iterable):
        self._l = a

    def __iter__(self):
        return iter(self._l)

  • 这样,我们就可以使用for循环来迭代它:
c = Container([1,2,3])

for i in c:
    print(i)
  • Python的迭代协议(在本章笔记最后附有迭代协议的详细内容)要求__iter__()返回一个特殊的迭代器对象,由该对象实现的__next__()方法来完成实际的迭代,在一个容器对象中迭代另一个容器的内容,叫做委托迭代。

4.3 用生成器创建新的迭代模式

  • 想要实现一个自定义的迭代模式,区别于range() reversed() 逆序迭代。range(start, stop, step)只能接收整数,而我们希望这个新的迭代模式可以接收浮点数:
def frange(start, stop, step):
	x = start
	while x < stop:
		yield x
		x += step

  • 这样,就可以得到一个浮点数序列:
f = frange(1.5, 4.5, 0.5)
print(list(f))
  • 结果:
[1.5, 2.0, 2.5, 3.0, 3.5, 4.0]
  • 只要出现了yield,函数就会转变为生成器,生成器中的元素只有在使用到时才生成

4.4 实现迭代协议

  • 以下的类定义了一个树的节点:
class Node:
    def __init__(self, val):
        self.val = val
        self._children = []

    def __iter__(self):
        return iter(self._children)

    def add_child(self, child):
        self._children.append(child)

    def deep_first(self):
        yield self
        for c in self:
            yield from c.deep_first()
  • 然后做一些初始化,构造出一棵树:
n = Node(0)
a = Node(1)
b = Node(2)
c = Node(3)
d = Node(4)
n.add_child(a)
n.add_child(b)
b.add_child(c)
a.add_child(d)
  • 就可以通过调用deep_first()方法来遍历这棵树了:
for i in n.deep_first():
    print(i.val)
  • 结果:
0
1
4
2
3
  • 如4.2节说的一样,依据迭代协议,实现了__iter__()方法使Node变成可迭代对象,在deep_first方法中,先是返回了自己,再使用for循环迭代_children即子节点。Node类实现了树的深度优先遍历

4.5 反向迭代

  • 使用内建的reversed()函数实现反向迭代:
a = [1,2,3,4]
for item in reversed(a):
    print(item)
  • 要使用reversed(), 待处理对象必须具有已知长度(列表、元组、字符串)或者实现了__reversed__()方法
  • 对于迭代器、生成器,无法直接使用reversed(),需要把它们转换为列表,但它们中的元素较多时,将会消耗大量内存

4.6 定义带有额外状态的生成器函数

  • 一个例子:如果要读取一个文本文件,我们一般这样写:
with open('abc.txt', 'r') as f:
    lines = f.readlines()
    for line in lines:
        print(line) # or you might do something else here
  • f.readlines()是一个生成器函数,这样我们就可以逐行获取文件的内容
  • 但如果我们不仅想获取文件的每一行,还想存储固定长度的历史记录,还可以清空这个历史记录,那么可以把生成器封装在类中
  • 生成器函数只要实现在__iter__()方法里即可:
  • deque是在第一章中提到的可以固定长度的双端队列
from collections import deque

class LineWithHistory:
    def __init__(self, lines, maxlen=3):
        self._lines = lines
        self.history = deque(maxlen=maxlen)

    def __iter__(self):
        for lineno, line in enumerate(self._lines, 1):
            self.history.append((lineno, line))
            yield line

    def clear(self):
        self.history.clear()
  • 我们的文本文件内容是这样的:
java
python
php
javascript
julia
  • 下面,读取文本文件的内容:


with open('abc.txt', 'r') as f:
    lines = LineWithHistory(f)
    for line in lines:
        print("Now the line is:", line)
        if 'php' in line:
            for his in lines.history:
                print('HISTORY: line: {} content: {}'.format(his[0], his[1]))
        if 'julia' in line:
            lines.clear()

    print(len(lines.history))
  • 我们逐行扫描文件的内容,并在当前行内容是php的时候,打印历史记录的内容
  • 在当内容是julia时,清空所有的历史记录
  • 这样,我们不仅迭代了生成器中的元素,还额外存储了一些与之相关的状态可供访问

4.7 对迭代器做切片操作

  • 一般的迭代器不可作切片处理:
>>> a = [1,2,3,4]
>>> b = iter(a)
>>> b[1:]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'list_iterator' object is not subscriptable
  • 可以用itertools.islice(Iterable, start, stop, step):
from itertools import islice

a = [1,2,3,4,5,6]
for i in islice(a, 2, 4):
    print(i)
  • islice的返回值是一个迭代器,其实它的逻辑就是丢弃目标迭代/生成器的前一部分元素,并从start开始,按step步长到索引结束位置,这么做会消费完目标迭代/生成器

4.8 跳过可迭代对象中的前一部分元素

  • 使用itertools.dropwhile函数:
from itertools import dropwhile

with open('abc.txt', 'r') as f:
    for l in dropwhile(lambda line: line.startswith('#'), f):
        print(l)
  • dropwhile 的第一个参数接收一个函数,定义抛弃元素的规则,以上代码可以跳过文件开头的“#”注释
  • 在我们明确想要跳过多少个元素时,可以使用4.7节中的itertools.islice(Iterable, start, stop, step),可以跳过start个元素

4.9 迭代所有可能的组合或排列

  • itertools模块中提供了3个函数(permutations, combinations, combinations_with_replacement)来进行排列组合的相关操作
  • itertools.permutations(Iterable), 接受一个可迭代对象,返回一个迭代器,包含了这个可迭代对象的全排列:
from itertools import permutations

for p in permutations([1,2,3]):
    print(p)
  • 结果:
(1, 2, 3)
(1, 3, 2)
(2, 1, 3)
(2, 3, 1)
(3, 1, 2)
(3, 2, 1)
  • 也可以再传入第二个参数n:itertools.permutations(Iterable, n), 则会求出长度为n的子集的全排列:
for p in permutations([1,2,3], 2):
    print(p)
  • 结果:
(1, 2)
(1, 3)
(2, 1)
(2, 3)
(3, 1)
(3, 2)
  • 要求出一个序列的长度为n的所有组合(不考虑元素间的顺序),使用combinations(Iterable, n)
from itertools import combinations

for c in combinations([1,2,3,4,5], 3):
    print(c)
  • 结果:
(1, 2, 3)
(1, 2, 4)
(1, 2, 5)
(1, 3, 4)
(1, 3, 5)
(1, 4, 5)
(2, 3, 4)
(2, 3, 5)
(2, 4, 5)
(3, 4, 5)
  • 使用combinations函数时,每个元素在同一组合里只能出现一次,如果要取消这一限制,使用combinations_with_replacement()函数

4.10 以索引-值对的形式迭代序列

  • 使用enumerate函数:
a = ['a', 'b', 'c']
for idx, item in enumerate(a):
	print("current index: {}, item: {}".format(idx, item))
  • 上面的代码迭代了列表a,除了逐个打印出每个元素以外,还打印出了每个元素的索引
  • 索引是从0开始的,如果希望改变索引的起始值,可以enumerate(a, 1), 这样索引就会从1开始

4.11 同时迭代多个序列

  • 想将两个Iterable同时迭代,例如,想将它们的内容按索相等引拼在一起,使用zip()函数
first_name = ['三', '四', '五']
last_name = ['张', '李', '王']

for f, l in zip(first_name, last_name):
    print(l, f)
  • 结果:
张 三
李 四
王 五
  • 如果两个序列不一样长,zip() 以最短的序列为准,如果first_name = [‘三’, ‘四’, ‘五’, ‘六’],结果仍和上面一样
  • 如果想改变这种行为,使用itertools.zip_longest(),这将以长的序列为准,缺失的元素默认以None填充,也可以用fillvalue参数指定填充值:
from itertools import zip_longest

first_name = ['三', '四', '五']
last_name = ['张', '李', '王', '陈']

for f, l in zip_longest(first_name, last_name, fillvalue='无名'):
    print(l, f)
  • 结果:
张 三
李 四
王 五
陈 无名
  • 使用dict(zip(a, b))可以产生一个字典
  • zip()可以接收多于2个序列作为参数

4.12 在不同的容器中迭代

  • 如果希望迭代多个序列,并对他们中的每一个元素作同种操作
  • 使用itertools.chain():
a = [1,2,3]
b = {'a', 'b', 'c'}
for i in chain(a, b):
    print(i)
  • 以上代码逐个打印出了列表a和集合b中的元素
  • 当所有的序列都是列表(或元组)时,使用“+”将两个列表(元组)拼接起来也可以实现同样的操作,但这样会产生一个新的序列,在待处理序列较大时,消耗内存大,显得低效;
  • 而且当序列是不同类型的时候,无法用"+"拼接;
  • 以上两点证明使用chain()函数是更好的方法。

4.13 创建处理数据的管道

  • 如果我们要对目录下的海量文件进行操作(得到目录下文件的名字、打开文件、对文件内容进行处理),由于数量过大,无法将数据全部加载到内存中
  • 可以用流水线的形式,即分解整个处理过程为若干个步骤,每个步骤的结果交给下一个步骤,类似于Unix的管道:
ps -ef | grep python
  • 使用生成器函数实现这样的管道,假设A, B, C分别是三个生成器函数,则用法看起来会是这样:
a = A()
b = B(a)
c = C(b)
# c 中包含了最终的结果, 可以通过迭代来使用其中的内容
for content in c:
	# do something
	pass
  • 每一次迭代,都只会取出每个生成器函数的一份数据,即调用一次yield

4.14 扁平化处理嵌套型的序列

  • 如果有一个列表,涉及了多层嵌套,看起来是这样的(实际上很少有这样奇怪的数据:)):
embedded_list = [1, [2, 3], [4, [5, [6, 7]], 8], 9]
  • 希望把它变成扁平化,即变成:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 写一个生成器函数进行处理:
from collections.abc import Iterable

def flatten(items, ignored_types=(bytes, str)):
    for item in items:
        if isinstance(item, Iterable) and not isinstance(item, ignored_types):
            yield from flatten(item, ignored_types)
        else:
            yield item
  • 上面的生成器函数中:
    • 对于序列中的每一项,判断它是不是一个可迭代对象,如果是,则说明这是一个嵌套的序列,对它继续调用flatten()
    • 如果不是一个可迭代对象,说明这是一个单独的元素,直接返回
    • 对于类似字符串、字节串这样的对象,它们也属于Iterable,但我们不希望将它们中的元素一个个的分解开,所以特别引入ignored_types对它们作判断。

4.15 合并多个有序序列,再对整个有序序列进行迭代

  • 例如,列表a和b:
a = [1, 6, 7, 9, 12]
b = [2, 3, 4, 5, 10]
  • 使用heapq.merge()函数,它返回了一个生成器:
for item in heapq.merge(a, b):
	print(item)
  • 结果:
1
2
3
4
5
6
7
9
10
12
  • 这比设计逻辑合并a, b 为一个新列表更节省内存开销。
  • merge函数可以接收多于两个的序列

4.16 用迭代器取代while循环

  • 一段涉及IO处理的代码:
CHUNKSIZE = 8192

def reader(s):
	while True:
		data = s.recv(CHUNKSIZE)
		if data == b'':
			break
		process_data(data)
  • 在以上代码中,我们逐块的读取字节数据,并调用process_data函数对数据进行处理
  • 把b’‘作为结束处理的条件——如果遇到了b’'则认为读取完毕,跳出循环
  • 使用iter()函数替代了while循环,把代码改成这样:
def reader(s):
	for chunk in iter(lambda: s.recv(CHUNKSIZE), b''):
		process(data)
  • iter()函数可以设置一个哨兵值,如果返回哨兵值,则终止迭代

本章使用的模块、方法:

next(Iterator, sentinel) # 取出迭代器的一个元素,sentinel为哨兵值
reversed() # 迭代已知长度的可迭代对象
itertools.islice(Iterator, start, stop) # 对迭代器切片
itertools.dropwhile(predicate, Iterable) # 跳过迭代对象的前一部分元素
itertools.permutations(Iterable, r) # 求排列
itertools.combinations(Iterable, r) # 求组合
zip(Iterables) # 合并多个可迭代对象,返回迭代器
itertools.zip_longest(Iterables, fillvalue) # 合并多个可迭代对象,以较长的为准,以fillvalue补齐空值
itertools.chain(Iterables) # 连接多个可迭代对象,返回迭代器
heapq.merge(Iterables) # 合并已排序的序列为一个新的排序序列
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值