什么是生成器

英文说法

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 inspect
# 是函数,也是生成器函数
print(inspect.isfunction(gen))
print(inspect.isgeneratorfunction(gen))

# 生成器函数不是generator,他的返回值是generator
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)>
1
2
# 代码对象
func.__code__
<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 inspect
def 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_refs
f2 = foo()
show_backrefs(foo.__code__)

svg

这个图就非常清晰的可以到函数运行时函数对象、代码对象、帧对象之间的关系
functionframe都引用了code对象
frame通过f2被引用,是因为foo()返回的frame对象赋值给了变量f2

函数嵌套调用

1
2
3
4
5
# 发生函数调用时的栈
from objgraph import show_refs
def bar():
return foo()
f1 = bar()
1
show_refs(f1, max_depth=2,too_many=2) # max_depth限制下深度,限制下宽度too_many

svg

可以看到函数嵌套调用的时候,函数运行帧是依次运行的,只有当 foo 的 frame 运行完,bar 的 frame 才能继续运行。这也与我们对普通函数嵌套调用的执行顺序的认知想印证。

生成器

生成器函数

1
2
3
4
5
import inspect

def gen_foo():
for _ in range(10):
yield inspect.currentframe()
1
2
# 生成器函数
show_refs(gen_foo, max_depth=2, too_many=2)

svg

可以看到,生成器函数其实跟普通函是一样的,有 code 对象。

生成器对象

1
2
# 生成器对象
show_refs(gen_foo())

svg

生成器对象保存了frame对象和code对象,这其实就是生成器能暂停然后继续运行的根本原因(对生成器对象执行 next 方法)。

1
2
3
4
5
6
7
# 生成器迭代过程中都是同一个frame对象
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)

1
func_a(gfg)

svg

1
func_b(gfg)

svg

由此可见:

  • 生成器函数并不直接运行,而是借助于生成器对象来间接运行。
  • 创建生成器对象的同时创建了帧对象,并且由生成器对象保持引用
  • 每次使用 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
sync_task_runner()
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 调用对应的任务。

总结

让一个函数可以多次迭代运行其中的代码,这是生成器最本质的作用,迭代产生的数据只是迭代执行代码的自然结果。