什么是生成器 英文说法
A function which returns a generator iterator Usually refers to a generator function, but may refer to a generator iterator in some contexts. In cases where the intended meaning isn’t clear, using the full terms avoids ambiguity.
翻译过来
一个返回生成器迭代器的函数,通常指的是生成器函数,但是在一定的语境下也可以指代生成器迭代器。为了避免歧义,推荐使用完整的术语。
1 2 3 4 5 6 def gen (): print ("hello" ) if 0 : yield print (gen, gen())
<function gen at 0x7f64f86a5750> <generator object gen at 0x7f64f4545a10>
根据上面示例就可以很好理解了,生成器函数返回的对象叫生成器对象,上述例子中,gen
是生成器函数,gen()
返回的是生成器对象。所以需要注意区分到底是生成器函数 还是生成器对象 。
1 2 3 4 5 6 7 8 import inspectprint (inspect.isfunction(gen))print (inspect.isgeneratorfunction(gen))print (inspect.isgenerator(gen))print (inspect.isgenerator(gen()))
True
True
False
True
我们也可以借助inspect
模块的相关方法,进一步理解生成器函数 和生成器对象 。
1 2 set (dir (gen())) - set (dir (object ))
{'__del__',
'__iter__',
'__name__',
'__next__',
'__qualname__',
'close',
'gi_code',
'gi_frame',
'gi_running',
'gi_yieldfrom',
'send',
'throw'}
可以看到生成器对象也实现了迭代器协议,也即实现了__iter__
和__next__
方法。
手动实现可迭代对象 可迭代对象就是能用for
遍历的对象,本质上是实现了__iter__
接口。
老方式 按照可迭代对象和迭代器的接口协议,分别实现对应的方法。
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 class MyCustomDataIterator : def __init__ (self, data ): self.data = data self.index = 0 def __iter__ (self ): return self def __next__ (self ): if self.index < self.data.size: self.index += 1 return self.data.get_value(self.index-1 ) else : raise StopIteration class MyCustomData : def __init__ (self ): self.data = [1 ,2 ,3 ,4 ] @property def size (self ): return len (self.data) def get_value (self, index ): return self.data[index] def __iter__ (self ): return MyCustomDataIterator(self)
1 2 for x in MyCustomData(): print (x)
1
2
3
4
新方式(yield) 使用yield
让__iter__
方法返回生成器对象,生成器对象本身就是一个迭代器,自然满足__iter__
接口的条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class MyCustomData : def __init__ (self ): self.data = [1 ,2 ,3 ,4 ] @property def size (self ): return len (self.data) def get_value (self, index ): return self.data[index] def __iter__ (self ): for x in self.data: yield x for x in MyCustomData(): print (x)
1
2
3
4
欧通函数 函数对象、代码对象 1 2 3 4 def func (x ): print (x) func
<function __main__.func(x)>
<code object func at 0x7fa121d4d210, file "/tmp/ipykernel_25636/30147105.py", line 2>
1 2 3 4 5 func_code = func.__code__ for attr in dir (func_code): if attr.startswith("co" ): print (f"{attr} \t: {getattr (func_code, attr)} " )
co_argcount : 1
co_cellvars : ()
co_code : b't\x00|\x00\x83\x01\x01\x00d\x00S\x00'
co_consts : (None,)
co_filename : /tmp/ipykernel_25636/30147105.py
co_firstlineno : 2
co_flags : 67
co_freevars : ()
co_kwonlyargcount : 0
co_lines : <built-in method co_lines of code object at 0x7fa121d4d210>
co_linetable : b'\x0c\x01'
co_lnotab : b'\x00\x01'
co_name : func
co_names : ('print',)
co_nlocals : 1
co_posonlyargcount : 0
co_stacksize : 2
co_varnames : ('x',)
函数运行 函数帧对象 函数对象和代码对象保存了函数的基本信息,当函数运行的时候,还需要一个对象来保存运行时的状态,这个对象就是「帧对象(Frame Object)」
1 2 3 4 5 !pip install objgraph import inspectdef foo (): return inspect.currentframe()
Requirement already satisfied: objgraph in /opt/conda/lib/python3.10/site-packages (3.5.0)
Requirement already satisfied: graphviz in /opt/conda/lib/python3.10/site-packages (from objgraph) (0.20.1)
1 2 3 4 from objgraph import show_backrefs,show_refsf2 = foo() show_backrefs(foo.__code__)
这个图就非常清晰的可以到函数运行时函数对象、代码对象、帧对象之间的关系function
和frame
都引用了code
对象frame
通过f2
被引用,是因为foo()
返回的frame
对象赋值给了变量f2
函数嵌套调用 1 2 3 4 5 from objgraph import show_refsdef bar (): return foo() f1 = bar()
1 show_refs(f1, max_depth=2 ,too_many=2 )
可以看到函数嵌套调用的时候,函数运行帧是依次运行的,只有当 foo 的 frame 运行完,bar 的 frame 才能继续运行。这也与我们对普通函数嵌套调用的执行顺序的认知想印证。
生成器 生成器函数 1 2 3 4 5 import inspectdef gen_foo (): for _ in range (10 ): yield inspect.currentframe()
1 2 show_refs(gen_foo, max_depth=2 , too_many=2 )
可以看到,生成器函数其实跟普通函是一样的,有 code 对象。
生成器对象
生成器对象保存了frame
对象和code
对象,这其实就是生成器能暂停然后继续运行的根本原因(对生成器对象执行 next 方法)。
1 2 3 4 5 6 7 gf = gen_foo() gi_frame = gf.gi_frame print (gi_frame)frames = list (gf) for f in frames: print (f is gi_frame)
<frame at 0x7f5d050bf100, file '/tmp/ipykernel_84/1501290856.py', line 3, code gen_foo>
True
True
True
True
True
True
True
True
True
True
生成器运行 1 2 3 4 5 def gen_frame_graph (): for _ in range (10 ): graph = show_refs(inspect.currentframe(), max_depth=3 , too_many=4 ) yield graph
1 2 3 4 5 6 7 8 gfg = gen_frame_graph() def func_a (g ): return next (g) def func_b (g ): return next (g)
由此可见:
生成器函数并不直接运行,而是借助于生成器对象来间接运行。
创建生成器对象的同时创建了帧对象,并且由生成器对象保持引用
每次使用 next()调用生成器时,就是将生成器引用的帧对象入栈
当 next()返回时,也就是代码遇到 yiel d 暂停的时候,就是将帧出栈。
直到迭代结束,帧最后一次出栈,并且被销毁
同步和异步 普通函数
调用函数:构建帧对象并入栈
函数执行结束:帧对象出栈并销毁
1 2 3 4 5 6 task_a = lambda :print (1 ) task_b = lambda :print (2 ) def sync_task_runner (): task_a() task_b()
1
2
普通函数只能同步运行多个任务,必须等先调用的任务运行结束,才能开始下一个任务。
生成器
创建生成器:构建帧对象
next 触发运行(多次):帧入栈
yield 获得结果(多次):帧出栈
迭代结束:帧出栈并销毁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def async_task (): yield 1 yield 2 yield 3 async_task_a = async_task() async_task_b = async_task() def async_task_runner (): print (next (async_task_a)) print (next (async_task_b)) print (next (async_task_b)) print (next (async_task_b)) print (next (async_task_a)) async_task_runner()
1
1
2
3
2
可以看到任务交替运行了,想要运行哪个任务,就使用 next 调用对应的任务。
总结 让一个函数可以多次迭代运行其中的代码,这是生成器最本质的作用,迭代产生的数据只是迭代执行代码的自然结果。