PythonCookbook第4章(迭代器和生成器)良好完成
迭代使Python中最强有力的特性之一。从高层次看,我们可以简单地把迭代看做是一个处理序列中元素的方式。
4.1手动访问迭代器中的元素
from collections import abc with open('/etc/passwd') as f: print(isinstance(f, abc.Iterator)) print(isinstance(f, abc.Generator)) try: while True: line = next(f) print(line, end='') except StopIteration: ...
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_4/t1_2.py True False ## # User Database
open对象是一个迭代器对象。
讨论:
内容太少,不写了
4.2 委托代理
问题
我们构建一个自定义的容器对象,对内部持有一个列表、元祖或者其他的可迭代对象。我们想让自己的新容器完成迭代操作。
解决:
迭代器需要有__iter__与__next__属性,通过iter函数执行返回一个迭代器是一个比较简单的好方法。
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) # for循环首先调用对象的该方法,然后通过__next__方法读取参数,读取完,返回信息StopIteration def __iter__(self): return iter(self._children) if __name__ == '__main__': root = Node(0) child1 = Node(1) child2 = Node(2) root.add_child(child1) root.add_child(child2) for ch in root: print(ch)
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_4/t2_2.py Node(1) Node(2) Process finished with exit code 0
讨论:
Python的迭代协议要求__iter__()返回一个特殊的迭代器对象,由该对象实现的__next__()方法来完成实际的迭代。
4.3 用生成器创建新的迭代模式
问题:
我们创建一个自己的迭代模式,使其区别于常见的内建函数(range(),recersed())
解决方案
创建一个生成器函数
def frange(start, stop, step): x = start while x < stop: yield x x += step if __name__ == '__main__': for n in frange(10, 12, 0.5): print(n)
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_4/t3_2.py 10 10.5 11.0 11.5 Process finished with exit code 0
讨论:
In [90]: def countdown(n): ...: print('Starting go count from', n) ...: while n > 0: ...: yield n ...: n -= 1 ...: print('Dnne') ...: In [91]: c= countdown(3) In [92]: # 生成器函数运行的时候不执行 In [93]: next(c) Starting go count from 3 Out[93]: 3 In [94]: # 第一次执行到yield n处 In [95]: next(c) Out[95]: 2 In [96]: # 程序向下执行寻找下一个yield处 In [97]: next(c) Out[97]: 1 In [98]: next(c) Dnne --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-98-e846efec376d> in <module> ----> 1 next(c) StopIteration: In [99]: # 当next向下执行未能找到yiled,函数返回,迭代结束。
函数中只要出现了yield语句就会将其转变成一个生成器。与普通函数不同,生成器只会在响应迭代操作才运作。
生成器函数只会在响应迭代过程中的"next"操作时才会运行。一旦生成器函数返回,迭代就停止了
4.4 实现迭代协议
问题:创建一个对象,希望他可以支持迭代操作,但是也希望能有一种简单的方式来实现迭代。
解决方案:
主要实现了一个可以以深度优先的模式遍历树的节点。
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): # 首先返回自己,然后循环读取自己,for循环调用__iter__返回子生的迭代器内的内容 # 再词调用对象的depth_first方法进行递归。其实用双层的for循环比用yield form好理解一些 # yield form当用作委派生成器的时候用更加好。 yield self for c in self: for i in c.depth_first(): yield i # 效果等同于上面的for循环,前面的yield from快忘光了 # 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)
/usr/local/bin/python3.7 /Users/shijianzhong/study/Fluent_Python/第十六章/coroaverager3.py 6 boys averaging 54.00kg 6 boys averaging 1.68m 6 girls averaging 44.00kg 6 girls averaging 1.58m Process finished with exit code 0
讨论:
书中还写以一种关联迭代器的写法,代码比较复杂,上面的用递归的方式,深度优先遍历对象,我对递归还是比较抵触的。
书中的代码还是上一下算了,写法可能复杂点,看能不能理解起来更加简单。
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): return DepthFirstIterator(self) class DepthFirstIterator: def __init__(self, start_node): self._node = start_node self._children_iter = None self._child_iter = None def __iter__(self): return self # 关键在与__next__具体的执行 def __next__(self): # 第一次肯定执行这个,返回初始化的DepthFirstIterator里的self,也就是Node实例 if self._children_iter is None: self._children_iter = iter(self._node) return self._node # 这个是第三步操作,从第二步的return那里过来 elif self._child_iter: try: nextchild = next(self._child_iter) return nextchild except StopIteration: self._child_iter = None return next(self) # 第二步,从self._children_iter取出一个对象,执行depth_first方法 else: # 执行这一步的时候已经产生了递归了,看的眼睛都涨死了 self._child_iter = next(self._children_iter).depth_first() # 对自己进行下一步操作 return next(self) if __name__ == '__main__': root = Node(0) child1 = Node(1) child2 = Node(2) root.add_child(child1) root.add_child(child2) c_child3 = Node(3) c_child3.add_child(Node(88)) child1.add_child(c_child3) child1.add_child(Node(4)) child2.add_child(Node(5)) for ch in root.depth_first(): print(ch)
第二种方法,代码更加复杂,而且从理解来看,一点也没的方便。
4.5 反向迭代
问题:
反向迭代序列中的元素
解决方案:
reversed函数实现
反向迭代只有待处理的对象拥有可确定的大小,或者对象实现了__reveresd__()方法时才能奏效。如果两个条件都无法满足,先转换为列表。
也就是说,不能对迭代器直接使用reversed函数。
讨论:
可以定义__reversed__方法的对象,给自己使用。
class Countdown: def __init__(self, start): self.start = start # 从开始的数字开始 def __iter__(self): n = self.start while n > 0: yield n n += 1 # 从1开始到初始化的数字结束 def __reversed__(self): n = 1 while n <= self.start: yield n n += 1 if __name__ == '__main__': c =Countdown(10) forward = iter(c) reveres = reversed(c) print(next(forward)) print(next(reveres))
4.6 定义带有额外状态的生成器函数
问题:
定义一个生成器函数,但是它涉及一些额外的状态,需要显示
解决方案:
定义一个类,需要显示的东西放在实例属性里面,生成器函数定义在__iter__里面,输出前几行的内容,很好的一个案例
from collections import deque class Line_History: def __init__(self, lines, histlen=3): self.lines = lines, self.history = deque(maxlen=histlen) def __iter__(self): for lineno, line in enumerate(self.lines, start=1): # 读取行号与内容放去双端队列里面去 self.history.append((lineno, line)) yield line def clear(self): self.history.clear() if __name__ == '__main__': with open('sometext') as f: # f是一个迭代器,我测试过 lines = Line_History(f) # 调用iter方法 for line in lines: if 'python' in line: # 输出对象属性的双端队列里面的内容 for lineno, hline in lines.history: print('{}:{}'.format(lineno, hline), end='')
讨论:
就是解决方案里面的第一句话
4.7对迭代器做切片操作
问题:
相对迭代器进行切片
解决方案:
itertools.islice函数
讨论:
islice是通过访问并丢弃所有起始索引之前的元素来实现的,之后的元素有islice对象产出。
操作islice对象的时候,其实就是在操作原来的对象,如果还需要倒回访问前面的数据,就应该先将数据转到列表中去。
4.8跳过可迭代对象中的前一部分元素
问题:
对一个可迭代对象处理,但对前面的几个元素不感兴趣,想把它们丢弃
解决方案:
itertools.dropwhile函数可以实现。
In [72]: from itertools import dropwhile
In [73]: dropwhile?
Init signature: dropwhile(self, /, *args, **kwargs)
Docstring:
dropwhile(predicate, iterable) --> dropwhile object
Drop items from the iterable while predicate(item) is true.
Afterwards, return every element until the iterable is exhausted.
Type: type
Subclasses:
第一个参数为函数,第二个参数为操作的迭代对象。
当满足函数的条件时,这些可迭代对象里面的元素将被忽略,直到出现不满足条件的元素出现,包含这个不满足条件的元素以及后面的元素都将不被筛选直接返回。
讨论:
dropwhile与islice都是很好用的函数,以后要记得。
4.9迭代所有的组合或排列
专门写过
combinations组合,不关心元素的位置
permutations排列,考虑元素的位置不同
4.10 以索引-值对的形式迭代序列
问题:
想处理一个序列,但是想记录下序列中当前处理到的元素索引。
解决方案:
enumerate是一个很好的函数,对于追踪文本操作还是非常有用的。上一个书中的经典代码
from collections import defaultdict # 创建一个默认字典 word_summary = defaultdict(list) with open('myfile.txt') as f: # 按行读取所有的内容 lines = f.readlines() for idx, line in enumerate(lines): # 每个单词变小写,前后取出空格 words = [w.strip().lower() for w in line.split()] # 读取单词放入,用单词做key将每一行放入value for word in words: word_summary[word].append(idx)
讨论:
enumerate()的返回值是一个enumerate对象实例,它是一个迭代器,可返回连续的元祖。元祖有一个索引值和对传入的序列iter以后调用next()而得到的值组成。
4.11 同时迭代多个序列
zip与itertools.zip_longest
前期已经详细介绍,略。
4.12 在不同的容器中进行迭代
问题:
需要对许多对象执行相同的操作,但是这些对象包含在不同的容器内,为了避免写出嵌套的循环处理,保持代码的可读性
解决方案:
itertools.chain
讨论:
for x in a+b: ... for x in chain(a, b): ...
第一种方式,需要a与b为同一种类型,而且会产生一个全新的序列,第二种方式的内存使用要小,因为chain返回的是一个迭代器
4.13 创建处理数据管道
问题:
我们要处理海量的数据,但是没办法将数据全部加载到内存中去
解决方案:
通过定义一系列小型的生成器函数,每个函数执行特定的独立任务
import os import fnmatch import gzip import bz2 import re def gen_find(filepat, top): ''' 找出需要处理的文件的绝对路径 ''' 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): # 打开文件,产出文件内容流 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): # for循环接收上一个生成器出来的文件流对象,通过yield from产出每一个行 for it in iterators: yield from it def gen_grep(pattern,lines): pat = re.compile(pattern) for line in lines: print(line) if pat.search(line): yield line if __name__ == '__main__': lognames = gen_find('1.txt','../chapter_4') files = gen_opener(lognames) lines = gen_concatenate(files) pylines = gen_grep('(.*)',lines) for line in pylines: print(line)
讨论:
通过前面的代码,我们可以分析看到,yield变现为数据的生产者,for表现为数据的消费者
每一个生成器函数里面,都有for循环来接收上一级的产出,然后通过yield语句为下一个生成器函数产出数据。
当最终的执行的时候,会调用每一层的生成器函数,这个感觉就想变速箱的齿轮,一个转动,每个都开始动起来了。
这里面最关键的是for循环接收上一次生成器函数的产出数据,和自身yield产出数据给下一层使用者。
4.14扁平化处理嵌套型的序列
问题:
一个嵌套的序列,需要扁平化处理一列单独的值
解决方案:
主要是通过递归的方式处理,书中还用了yield from
from collections.abc 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) # 第二种写法 for i in flatten(x): yield i else: yield x items = [1, 2, [3,[4,]],5] if __name__ == '__main__': print(list(flatten(items)))
讨论:
内容已经在代码里的,yield from在流畅的Python中有着更加详细的介绍,书中的写法只不过用到了其中的一点点功能,但对于递归,我是真的头痛,想多了脑子就想炸了。
从这里简单的用法来看,yield from会将一个可迭代对象变成迭代器,并产出迭代器内部的值(这里用到的功能)。
4.15 合并多个有序序列,再对整个有序序列进行迭代
问题:
有两个有序的序列,相对它们合并在一起之后的有序序列进行迭代
解决方案:
heapq.merge函数实现,返回一个生成器,非常节省内存
In [1]: import heapq In [2]: l1 = [1,2,3,4,] In [3]: l2 = [3,4,5,6] In [4]: l3 = heapq.merge(l1,l2) In [5]: l3 Out[5]: <generator object merge at 0x1060af5d0> In [6]: list(l3) Out[6]: [1, 2, 3, 3, 4, 4, 5, 6]
讨论:
heapq.merge的迭代性质意味着它对所有提供的序列不会做一次性读取。这意味着可以利用它处理非常长的序列,而开销非常小。
heapq.mergr要求输入的序列都是有序的。
4.16用迭代器取代while循环
问题:
我们的代码采用while循环来迭代处理数据,因为这其中涉及调用某个函数或有某种不常见的测试条件,而这些东西没法归类为常见的迭代模式。
解决方法:
iter(func,condition),通过iter的哨兵作为停止条件。
import random CHUNKSIZE = 8192 def reader(s): while True: data = s.recv(CHUNKSIZE) if data == b'': break process_data(data) # 用iter写 def reader(s): for chuck in iter(lambda :s.recv(CHUNKSIZE), b''): process_data(chuck) if __name__ == '__main__': # 自己回忆了一下,以前流畅的Python中的案例。 for i in iter(lambda :random.randint(1,10),8): print(i)
讨论:
iter()可以接收一个无参的可调用对象以及一个哨兵(结束)值作为输入。当以这种方式使用时,iter()会创建一个迭代器,然后重复调用用户提供的可调用对象,直到它返回哨兵值为止。
这个在涉及到I/O的问题有很好的效果。