生成器

生成器函数是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def gen_func():
yield 1


def func():
return 1


if __name__ == '__main__':
result1 = gen_func()
print(result1)
print(next(result1))

result2 = func()
print(result2)

只要函数中有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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

def fib(n):
"""
斐波那契 1 1 2 3 5 8
@:param n: 第几项
"""
if n <= 2:
return 1
else:
return fib(n - 1) + fib(n - 2)


def fib2(n):
"""非递归方式"""
flag, a, b = 0, 0, 1
result = []

while flag < n:
result.append(b)
a, b = b, a + b
flag += 1

return result


def fib_gen(n):
"""斐波那契生成器,内存非常小,只有计算的时候才会产生新的值"""
flag, a, b = 0, 0, 1
while flag < n:
yield b
a, b = b, a + b
flag += 1


if __name__ == '__main__':
print(fib(10))
print(fib2(10))

for item in fib_gen(10):
print(item)

Python 的函数原理

Python.exe 会调用 C 语言函数PyEval_EvalFrameEx去执行foo函数,首先会创建一个栈帧(stack frame),示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
def foo():
bar()


def bar():
pass


if __name__ == '__main__':
import dis

print(dis.dis(foo))

使用dis.dis()函数可以查看函数的字节码

1
2
3
4
5
6
  2           0 LOAD_GLOBAL              0 (bar)
2 CALL_FUNCTION 0
4 POP_TOP
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
None

Python 一切皆对象,栈帧为对象,字节码也为对象

先从全局global加载bar函数,然后调用bar函数,之后从弹出栈顶元素(暂时不清楚是啥,后面清楚了再补充),之后加载一个常量const,由于函数没有返回值,因此加载None,然后在返回一个 None。

foo调用bar的时候,又会创建一个栈帧,然后Python将控制权交给子函数的栈帧对象,所有栈帧都是分配在堆内存上(堆内存必须要手动释放,否则会停留在内存中),因此栈帧是可以独立于调用者存在的,比如上面的foo函数就是调用者,如果foo函数退出了,Python还是可以调用到bar,下面是示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import inspect

frame = None


def foo():
bar()


def bar():
global frame

# 获取当前的栈帧
frame = inspect.currentframe()


foo()
print(frame)
print(frame.f_code.co_name)

# 获取调用者的栈帧
caller_frame = frame.f_back
print(caller_frame.f_code.co_name)

这里foo执行完成后,还是可以拿到bar函数的栈帧。

CPython 的 C 栈

生成器就是利用了 Python 函数的栈帧在堆内存上的特性

此时再看生成器的代码:

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

def gen_func():
yield 1
a = 1
yield 2
b = 2
return 3
yield 4

# 注:这里的return是用来终止函数的,不会有返回值,所以yield 4不会执行

if __name__ == '__main__':
import dis

print(dis.dis(gen_func))

gen = gen_func()

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)

结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 40           0 LOAD_CONST               1 (1)
2 YIELD_VALUE
4 POP_TOP

41 6 LOAD_CONST 1 (1)
8 STORE_FAST 0 (a)

42 10 LOAD_CONST 2 (2)
12 YIELD_VALUE
14 POP_TOP

43 16 LOAD_CONST 2 (2)
18 STORE_FAST 1 (b)

44 20 LOAD_CONST 3 (3)
22 RETURN_VALUE
None

该函数的栈情况:

这里我们可以看到在 CPython 中,代码,生成器,栈帧,都是对象PyCodeObject, PyGenObject, PyFrameObject

f_lasti会记录最近一次执行代码的地方,也就是yield之前的一句,f_locals会记录yield之前的局部变量

协程的原理就是获取到gen,然后恢复它的执行。