迭代器和生成器是python异步IO(协程)并发编程的基础,尤其重要的是生成器。生成器在大部分场景中是用来产出数据的,即yield,但其实还有两个重要的应用,分别是“yield from”和send()函数,二者在协程中应用较多。
python的迭代协议
迭代器是访问集合内元素的一种方式,一般用来遍历数据。在编程规范中,迭代器模式是经典设计模式的一种。不同于以下标的方式访问(原理如之前所提到的__getitem__魔法函数),迭代器是不能返回的,只能一条一条的产生数据,提供了一种“惰性”访问数据的方式。for循环的基础就是迭代器,能够做for循环操作的类型都是实现了迭代协议的。迭代协议就是“__iter__”魔法方法,实现了__iter__方法就是一个可迭代类型,可迭代类型和迭代器不是一个概念。
1 2 3 4 5 |
from collections.abc import Iterable, Iterator #后者是前者的子类,并且在前者实现的__iter__的基础上实现了__next__,即前者是“可迭代“,后者是迭代器,迭代器必须要实现__next__。 a =[1, 2] print(isinstance(a, Iterable)) print(isinstance(a, Iterator)) |
迭代器和可迭代对象
python中有内置函数“iter()”,可以接收一个可迭代对象,返回一个迭代器,如下例所示:
1 2 3 4 5 |
from collections.abc import Iterable, Iterator a =[1, 2] iter_obj = iter(a) print(isinstance(iter_obj , Iterable)) print(isinstance(iter_obj , Iterator)) |
python对对象进行for循环,其内部实现逻辑也是类似上面的原理,会调用iter()内置函数,尝试查找对象的__iter__()方法,如果查找失败则会“退化”查找__getitem__()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class Company(object): def __init__(self, employee_list): self.employee = employee_list def __getitem__(self, item): return self.employee[item] def __iter__(self): return 1 company = Company(["Tom", "Bob", "Jack"]) for item in company: print(item) my_itor = iter(company) #将上面的例子使用自定义迭代器的方式实现 class MyIterator(Iterator): def __init__(self, employee_list): self.iter_list = employee_list self.index = 0 def __next__(self): try: word = self.iter_list[self.index] except IndexError: raise StopIteration self.index += 1 return word class Company(object): def __init__(self, employee_list): self.employee = employee_list def __iter__(self): return MyIterator(self.employee) |
生成器函数的使用
掌握生成器是理解协程的基础。生成器函数就是包含有“yield”关键字的函数,如下例。
1 2 |
def gen_func(): yield |
不同于return语句在函数只要return一般是无法继续return的,生成器函数返回的是一个生成器对象,是一个迭代器,所以yield后续还可以继续yiled。
1 2 3 4 5 6 7 |
def gen_func(): yield 1 yield 2 yield 3 gen = gen_func() for i in gen: print(i) |
这是python语法中非常精妙的设计,首先利用yield关键字或者说生成器实现协程成为可能,然后也为惰性求值(延迟求值)提供了可能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#计算斐波那契数列 def fib(index): if index <= 2: return 1 else: return fib(index-1) + fib(index-2) #如果我们想要看到斐波那契数列的计算过程 def fib2(index): re_list = [] n,a,b = 0,0,1 while n<index: re_list.append(b) a,b = b, a+b n += 1 return re_list #如果index很大,re_list会非常消耗内存,可以使用生成器 def gen_fib(index): n,a,b = 0,0,1 while n<index: yield b a,b = b, a+b n += 1 for data in gen_fib(10): print (data) |
生成器的原理
python函数的运行原理是:python解释器(python.exe,是使用C语言实现的)会调用“PyEval_EvalFramEx(C语言实现的函数)”方法去执行脚本中函数,在运行脚本中函数之前,会首先创建一个栈帧(stack frame,或者成为堆栈)保存上下。python中一切皆对象,栈帧也是一个对象,该栈帧对象会将脚本中函数变成一个字节码对象,如下例,我们可以使用dis模块查看函数的字节码。
1 2 3 4 5 6 7 |
def foo(): bar() def bar(): pass import dis dis.dis(foo) |
继续上文,在运行脚本内函数,如foo函数之前,首先会创建栈帧对象,在栈帧对象的上下文中去运行字节码,函数的字节码是全局唯一的,因为函数是全局唯一的。而在foo中调用子函数bar函数时,又会创建一个栈帧,然后将PyEval_EvalFramEx函数的控制权交给这个新建栈帧,在新建栈帧的上下文中运行bar的字节码,机制类似于“递归”。需要注意的是,所有栈帧都是分配在堆内存上,堆内存的特性就是不去释放就会一直存在在内存中,这就决定了栈帧可以独立于调用者存在。
1 2 3 4 5 6 7 8 9 10 11 |
import inspect frame = None def foo(): bar() def bar(): global frame frame = inspect.currentframe() foo() print(frame.f_code.co_name) caller_frame = frame.f_back print(caller_frame.f_code.co_name) |

生成器对象的原理就是应用了python函数调用的过程,最关键的是利用了“所有的栈帧都是分配在堆内存上的”,这样生成器才有了实现的可能。生成器对象实际上是对PyFrameObject对象进行了封装,其结构如下图:

如上,其中“f_lasti”会指向最近执行的字节码位置,“f_locals”会存在变量字典。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def gen_func(): yield 1 name = "bobby" yield 2 age = 30 return "imooc" #在生成器函数中也可以return,在早期的python版本中不支持,会报错; import dis gen = gen_func() print (dis.dis(gen)) print(gen.gi_frame.f_lasti) print(gen.gi_frame.f_locals) next(gen) print(gen.gi_frame.f_lasti) print(gen.gi_frame.f_locals) next(gen) print(gen.gi_frame.f_lasti) print(gen.gi_frame.f_locals) |
利用上面这种机制,python利用生成器对函数的暂停(通过yield)和继续前进就有了理论基础,因为每次操作都会记录执行位置和localos变量,所以可以实现对函数整个运行过程的控制。而生成器对象也是分配在堆内存中的,所以可以独立于调用存在。由于gen_func()每次调用都会重新生成一个栈帧对象,所以只要有这个生成器对象我们就可以控制函数的运行,所以在任何函数、任何模块中只要拿到这个生成器对象,我们都可以恢复、暂停其运行,正因为我们可以在任何地方控制它,才有了“协程”概念的理论基础。
- 通过UserList来学习生成器的应用
如上文所述,在对可迭代对象(如list)进行for遍历时,python是调用了内置的“iter()”方法,首先查找对象的__iter__方法是否实现,如果没有则退化到查找__getitem__方法。其实现逻辑我们可以从源码中查看,python的list是使用C语言实现的,无法查看源码,python提供了一个由python语言实现的list,即UserList,其用python解释了list是如何实现的(用python实现了一遍),另外也可以被用户继承,因为有些场景我们需要继承list(不要继承全局的list,因为其使用C语言实现,内部有很多优化,很多关键的方法不允许覆盖)。
1 2 3 4 5 6 7 8 9 10 11 |
from collections import UserList UserList-->MutableSequence-->Sequence-->Reversible, Collection,在Sequence种实现了__iter__方法 def __iter__(self): i = 0 try: while True: v = self[i] yield v i += 1 except IndexError: return |
- 利用生成器读取大文件
在python中使用open()、readlines()读取超大文件(如500G)时一次性加载是不可行的,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def myreadlines(f, newline): buf = "" while True: while newline in buf: pos = buf.index(newline) yield buf[:pos] buf = buf[pos + len(newline):] chunk = f.read(4096) if not chunk: #说明已经读到了文件结尾 yield buf break buf += chunk with open("input.txt") as f: for line in myreadlines(f, "{|}"): print (line) |
转载请注明:北凉柿子 » Python高级编程和异步I/O并发编程笔记 8 迭代器和生成器