《Python学习笔记本》第五章 迭代器 笔记以及摘要(完结)
迭代器的概念
迭代器是指重复从对象中获取数据,直到结束。而所谓迭代协议,概括起来就是用__iter__方法返回一个实现了__next__方法的迭代器对象。
实现__iter__方法,表示目标尾可迭代(iterable)类型,允许执行手动或自动迭代操作。
通过该方法新建并返回一个迭代器(iterator)实例。随后,通过调用iterator__next__依次返回结果,直至抛出StopIteration异常表示结束。
可迭代类型未必就是序列数据,其也可能是按前后顺序操作的栈、队列、以及随机返回值的哈希表,甚至是未知终点的网络数据流。
流畅的Python书中说过能被iter函数作为单个参数返回的就是可迭代对象,也可认为具有__iter__或者__getitem__方法的就为可迭代对象
辅助函数
通过iter
In [24]: class Date: ...: def __init__(self,n): ...: self.data = range(n) ...: def __iter__(self): ...: return iter(self.data) ...: In [25]: Date(2).__iter__() Out[25]: <range_iterator at 0x114255ea0> In [26]: d = Date(3) In [27]: a = iter(d) In [28]: next(a) Out[28]: 0 In [29]: next(a) Out[29]: 1 In [30]:
作为辅助函数,iter还可为序列对象(__getitem__)自动创建迭代器包装
In [30]: class Data: ...: def __init__(self,n): ...: self.n = n ...: def __getitem__(self,index): ...: if index<0 or index>=self.n: raise IndexError ...: return index+100 ...: In [31]: d = Data(20) In [32]: d[10] Out[32]: 110 In [33]: d[10] Out[33]: 110 In [35]: dd = iter(Data(20)) In [36]: dd Out[36]: <iterator at 0x11402c410> In [37]: next(dd) Out[37]: 100 In [38]: next(dd) Out[38]: 101 In [39]: next(dd) Out[39]: 102 In [40]: next(dd) Out[40]: 103 In [41]:
itet以后的next取值,index从0开始自动+1
iter 甚至还对函数、方法等可调用类型(callable)进行包装。流畅的Python一书中叫哨兵(检查一个函数的输出,如果输出等于预设值停止)
iter可用于网络和文件等I/O数据接收,比起循环语句更优雅一些
In [41]: x = lambda :input('n: ') In [42]: for i in iter(x,'end'):print(i) n: 123 123 n: 432 432 n: 23 23 n: 234 234 n: end In [43]:
通过while循环读取迭代器不写了,就是一个StopIteration
自动迭代
对于for循环语句,编译器会生成迭代相关指令,以实现对协议方法的调用。
In [45]: def test(): ...: for i in [1,2]:print(i) ...: In [46]: import dis In [47]: dis.dis(test) 2 0 SETUP_LOOP 20 (to 22) 2 LOAD_CONST 1 ((1, 2)) 4 GET_ITER # 调用__iter__返回迭代器对象(或包装) >> 6 FOR_ITER 12 (to 20) # 调用__next__返回数据,结束则跳转 8 STORE_FAST 0 (i) 10 LOAD_GLOBAL 0 (print) 12 LOAD_FAST 0 (i) 14 CALL_FUNCTION 1 16 POP_TOP 18 JUMP_ABSOLUTE 6 # 继续迭代 >> 20 POP_BLOCK # 迭代结束 >> 22 LOAD_CONST 0 (None) 24 RETURN_VALUE In [48]:
设计意图
尽管列表、字典等容器类型实现了迭代器协议。但本质上,两者不属于同一层面。迭代器不仅是一种数据读取方法,而更多的是一种设计模式。
容器的核心是存储,其围绕数据提供操作方法,这是与用户逻辑无关的开放类型。而迭代器的重点是逻辑控制。调用方发出请求,随后决策有迭代器决定。数据内敛,抽象和实现分离。
生成器
生成器(generator)是迭代器的进化版本,用函数和表达式替代接口方法。其不仅简化了编码过程,还提供了更多控制能力用于复制计算。
生成器函数的特殊之处在与,其内部以yield返回迭代数据。这与普通函数不同,无论内部逻辑如何,其函数调用总是返回生成器对象。随后以普通迭代器方式继续操作。
In [1]: def test(): ...: yield 1 ...: yield 2 ...: In [2]: test() Out[2]: <generator object test at 0x10f51d5d0> In [3]: test Out[3]: <function __main__.test()> In [4]: test() Out[4]: <generator object test at 0x10f51d6d0> In [5]: for i in test():print(i) 1 2 In [6]:
每条yield语句对应一次__next__调用,可分为多提哦,或出现在循环语句中。只要结束函数流畅,就相当与抛出迭代终止异常
In [6]: def test(): ...: for i in range(10): ...: yield i+100 ...: if i >= 1: return ...: In [7]: x = test() In [8]: next(x) Out[8]: 100 In [9]: next(x) Out[9]: 101 In [10]: next(x) --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-10-92de4e9f6b1e> in <module> ----> 1 next(x) StopIteration: In [11]:
一旦生成器接触到return就直接返回StopIteration异常。
子迭代器
如果数据员本身就是可迭代对象,那么可以使用yield from 子迭代器语句。和for循环内用yield并无不同,只是语法更加简练。
yield from在Python的异常编程中会用到比较多。
In [11]: def test(): ...: yield from 'ab' ...: yield from range(3) ...: In [12]: for o in test(): print(o) a b 0 1 2 In [13]:
生成器表达式
除了小括号与列表推导式不同,另外的使用都一样
执行
首先,编译器会为生成器函数添加标记。对此类函数,解释器并不直接执行。而是将栈帧和代码作为参数,创建生成器实例。
In [22]: def test(n): ...: print('gen.start') ...: for i in range(n): ...: print(f'gen.yield{i}') ...: yield i ...: print('gen.resume') ...: In [23]: test.__code__.co_flags # 返回67为函数,返回99为生成器 Out[23]: 99 In [24]: import inspect In [25]: inspect.isgeneratorfunction(test) Out[25]: True In [26]:
简单一点说,就是以通用生成器类型为模板,实现外部迭代器协议。在内部通过栈帧和代码参数,实现对用户代码的调用。
In [27]: x = test(2) In [28]: x.gi_frame.f_locals # 栈帧内存储的函数调用参数 Out[28]: {'n': 2} In [29]: x.gi_code # 关联用户函数 Out[29]: <code object test at 0x110ba3810, file "<ipython-input-22-2237029e1f7a>", line 1> In [30]:
In [30]: x.__next__ # 实现迭代器协议方法 Out[30]: <method-wrapper '__next__' of generator object at 0x111552a50> In [31]:
所谓函数调用,不过是错觉。这也算是解释执行的好处,起码不用插入额外的汇编代码。接下来,生成器对象在第一次__next__调用时触发,进入用户函数执行。
In [31]: next(x) gen.start gen.yield0 Out[31]: 0 In [32]:
当执行到yield指令时,在设置好返回值后,解释器保存线程状态,并挂起当前函数流程。只有再次调用__next__方法时,才能恢复状态,继续执行。
如此,以yield为切换分界线,往复交替,直到函数结束。
执行状态保存在用户栈帧内,系统线程算是无状态多路复用,切换操作自然很简单。
In [32]: next(x) gen.resume gen.yield1 Out[32]: 1 In [33]: next(x) gen.resume --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-33-92de4e9f6b1e> in <module> ----> 1 next(x) StopIteration: In [34]:
方法
生成器的还具有提供双向通信能力。生成器不在是简单的数据提供方,其还可作为接收方存在。生成器甚至能在外部停止迭代,或发送信号实现重置等自定义行为。
方法send除可像yield传送数据外,其他和next完全一致。不过发生之前,须确保生成器已经启动。因为只有如此,才会进入函数执行流程,才会对外提供数据和交互。
In [34]: def test(): ...: while True: ...: v = yield 200 ...: print(f'resume{v}') ...: In [35]: x= test() In [36]: x.send(None) Out[36]: 200 In [37]: x.send(1) resume1 Out[37]: 200 In [38]: x.send(432) resume432 Out[38]: 200 In [39]:
对于生成器函数而言,挂起点是一个安全位置,相关状态被临时冻结。至于交互方式,只要将要发送的数据,或者其他状态标记栈帧的指定位置,随后由解释器决定如何处理既可。
调用close方法,解释器将终止生成器迭代。
该方法在生成器函数内部应发GeneratorExit异常,通知解释器结束执行。此异常无法捕获,但不影响finally的执行。
In [39]: def test(): ...: for i in range(10): ...: try: ...: yield i ...: finally: ...: print('finally') ...: In [40]: x = test() In [41]: next(x) Out[41]: 0 In [42]: next(x) finally Out[42]: 1 In [43]: x.close() # 不影响finally执行 finally In [44]: next(x) --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-44-92de4e9f6b1e> in <module> ----> 1 next(x) StopIteration: In [45]:
还可像send发送数据那样,向生成器throw执行异常作为信号。
In [47]: def test(): ...: while True: ...: try: ...: v = yield ...: print(f'recv:{v}') ...: except ResetException: ...: print('reset.') ...: except ExitException: ...: print('exit.') ...: return # 直接返回值,然后报StopIteration ...: In [48]: x = test() In [49]: x.send(None) In [50]: x.throw(ResetException) reset. In [51]: x.send(1) recv:1 In [52]: x.send(2) recv:2 In [53]: x.throw(ExitException) exit. --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-53-e26b821be143> in <module> ----> 1 x.throw(ExitException) StopIteration:
模式
借助生成器切换执行功能,改善程序结构设计。
在不借助并发框架的情况下,实现生产、消费协作
消费者启动后,使用yield讲执行权交给生产者,等待其发送数据后激活处理。
如果有多个消费者,或数据处理时间较长,建议使用专业并发方案。
In [108]: def consumer(): ...: while True: ...: v = yield ...: print(f'consume: {v}') ...: In [109]: def producer(c): ...: for i in range(10,13): ...: c.send(i) ...: In [110]: c = consumer() In [111]: c.__next__() # 预激活生成器 In [112]: producer(c) # 给生成器传递参数 consume: 10 consume: 11 consume: 12 In [113]:
消除回调
回调函数(callback)是常见异步接口设计方式。调用者在烦气请求后,不再阻塞等待结果返回,而改由异步服务调用预先注册的函数来完成后续处理。
设计一个简单异步服务示例,然后尝试用生成器清除回调函数。
import time import threading def target(request, callback): s = time.time() request() time.sleep(2) callback(f"done: {time.time() - s}") def request(): print('start') def callback(x): # 回调函数 print(x) def service(request, callback): threading.Thread(target=target, args=(request, callback)).start() if __name__ == '__main__': service(request, callback) print('do something')
上面是普通的回调函数使用。
import time import threading def target(fn): # 目标函数将协程当成参数进行传入 try: s = time.time() g = fn() next(g) time.sleep(2) g.send(f'done: {time.time() - s}') # 目标函数将参数传递给协程 except StopIteration: ... def request(): # 将协程传入目标函数进行使用。 print('start') x = yield print(x) def service(fn): threading.Thread(target=target, args=(fn,)).start() if __name__ == '__main__': service(request) print('do something')
从代码中可以看出来还是把生成器当做参数扔给了一条线程进行处理,避免了写回调函数。
协程
协程(coroutine)以协作调度方式,在单个线程上切换执行并发任务。配合异步接口,可将I/O阻塞时间用来执行更多任务。由于其在用户空间实现,因此也被称作用户线程。
理论上,任何具备yield功能的语言都能轻松实现携程,基于性能考虑,专业异步框架会选用greenlet之类的协程实现
def sched(*tasks): tasks = list(map(lambda t: t(), tasks)) # 执行函数变成生成器,实现协程 while tasks: try: t = tasks.pop(0) # 取出协程 t.send(None) # 等价与next(t)执行协程 tasks.append(t) # 如果还有yield就继续放入任务队列 except StopIteration: ... from functools import partial def task(id, n, m): for i in range(n, m): print(f'{id}: {i}') # 制作工作任务行数 yield # 编程生成器 t1 = partial(task, 1, 10, 13) # 变成偏函数,由于执行后变成携程 t2 = partial(task, 2, 30, 33) sched(t1, t2)
这是一个写的非常不错的示例。
5.4函数式编程(高阶函数,把函数当成对象进行传递)
函数式编程(Functional Programming)中的"函数"更趋近于数学概念,以计算表达式代替命令式语句。强调逐级结果推导,而非执行过程。
函数式编程要求函数为第一类型,可作为参数和返回值传递。需要时,可用闭包构成带有上下文状态的逻辑返回。其通常不实用独立变量,
所有状态以参数传递,用嵌套或链式调用代替过程语句。最好时没有外部依赖的纯函数,且参数不可变,仅以结果带入下一集运算。
相比于命令式代码,函数式风格更精简,更易组合,且可替换。其对单元测试和并发编程友好,引用且没有副作用。
函数式编程的三大特征:第一类型函数、不可变数据、尾递归优化。
函数式编程只是一种编程范式,多数语言都支持或部分支持它。
迭代:map;聚合:zip;累积:reduce;过滤:filter;判断:all、any