# 1. Python中函数的工作原理
"""
"""
def foo():
bar()
def bar():
pass
# python.exe 是用c语言写的,它会用一个叫做PyEval_EvalFramEx的函数(c函数)去执行foo函数。即python是在C之上运行,
# 它会首先创建一个栈帧(stack frame),这个栈帧实际上是上下文
"""
python中一切皆对象,栈帧也是对象,在栈帧的上下文里面去运行字节码。字节码是全局唯一的,
函数foo也是全局唯一的。 当foo调用子函数bar,又会创建一个栈帧,然后将函数bar的控制权
交给栈帧对象。该栈帧会运行bar这个函数的字节码;
所有的栈帧都是分配在堆内存上,堆的特性是:如果不去释放它,它就会一致存在于内存中,这
就决定了栈帧可以独立于调用者存在。即foo这个调用者不存在或退出foo这个函数也没有关系,
它仍在内存当中,只需要有指针指向这段内存,就可以对它进行控制。这个特性就决定了,可以
对函数执行过程的控制可以非常精确。
"""
import dis
print(dis.dis(foo)) # 可以查看字节码对象是什么样子的。
6 0 LOAD_GLOBAL 0 (bar)
2 CALL_FUNCTION 0
4 POP_TOP
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
None
# 首先LOAD_GLOBAL,即(bar)这个函数load进来。
# 然后调用这个bar函数CALL_FUNCTION ,是一个字节码
# POP_TOP 从栈的顶端将元素打印出来
# LOAD_CONST 由于foo函数没有return,会把(None)load进来
# RETURN_VALUE, 将None Return回去
import inspect
frame = None
def foo():
bar()
def bar():
global frame
frame = inspect.currentframe() # 将bar当前的frame赋值给全局变量
foo()
print(frame.f_code.co_name) # bar
# 此处,即使foo函数已经退出,我们仍然可以得到栈帧里面的data
caller_frame = frame.f_back
print(caller_frame.f_code.co_name) # foo
# 此处,虽然foo函数已经退出/运行完成,我们仍然可以得到当时运行时的栈帧
'''
静态语言的函数调用是栈的形式,函数调用完成,整个内存空间全部会销毁
动态语言的函数调用是堆的形式,函数调用完成,整个内存空间不会销毁,我们可以控制它。
'''
生成器对象,正是利用了python函数调用的过程。利用了栈帧对象是分配在堆内存中这个特性。
import dis
def gen_func():
yield 1
name = "Tom-1"
yield 2
age = 18
return "ABC" # 生成器函数可以像普通函数一样,return一个值。
gen = gen_func()
print(dis.dis(gen))
'''
python解释器会编译它的字节码,遇到生成器函数定义时,会在函数体内找到yield关键词,就会知道这是一个生成器函数,不是普通函数
就会设定一个标记,来标记该函数,标记之后,当我们调用该生成器函数时,会返回一个生成器对象,该生成器对象是在PyFrameObject与
PyCodeObject基础上,封装了一层PyGenObject,即python生成器对象。代码的字节码放在PyGenObject中,PyFrameObject里面存放的是f_lasti,它指向最近执行的代码。
当我们每次对生成器进行调用时,它会运行到yield就会停止,停止之后,f_lasti就会记录最近执行的字节码位置,以及locals变量f_locals
'''
25 0 LOAD_CONST 1 (1)
2 YIELD_VALUE
4 POP_TOP
26 6 LOAD_CONST 2 ('Tom-1')
8 STORE_FAST 0 (name)
27 10 LOAD_CONST 3 (2)
12 YIELD_VALUE
14 POP_TOP
28 16 LOAD_CONST 4 (18)
18 STORE_FAST 1 (age)
29 20 LOAD_CONST 5 ('ABC')
22 RETURN_VALUE
None
# 27 10 LOAD_CONST 3 (2) 此处yield第二个值2
import dis
def gen_func():
yield 1
name = "Tom-1"
yield 2
age = 18
return "ABC" # 生成器函数可以像普通函数一样,return一个值。
gen = gen_func()
print(dis.dis(gen))
print(gen.gi_frame.f_lasti) # -1 # 没有执行时的状态
print(gen.gi_frame.f_locals) # {} # 没有执行时的状态
next(gen) # 通过next 调用
print(gen.gi_frame.f_lasti) # 2 指向第1个字节码位置,数字是2,YIELD VALUE,实际上就是yield 1
print(gen.gi_frame.f_locals) # {}
next(gen) # 通过next再次调用
print(gen.gi_frame.f_lasti) # 12 指向第2个字节码位置,数字是12,YIELD VALUE,实际上就是yield 2
print(gen.gi_frame.f_locals) # {'name': 'Tom-1'}
'''
# 通过yield暂停,此处即有了控制函数暂停与继续的基础。
# 生成器对象分配在对内存中,类似于之前的栈帧,可以独立于调用者单独存在,每次调用生成器对象,就会重新生成一个栈帧对象。
只要拿到生成器对象,就可以继续控制它向前走。我们可以在任何函数/模块中,只要拿到生成器对象,都可以去恢复它,暂停它。
正是因为在任何时候都可以控制它,所以才有了后面协程的概念。
由于有了这个理论基础,利用f_lasti与f_locals两个变量,它就使得可以在生成器里面不断的循环同一个函数。由于我们以及通过
yield控制了函数的暂停,不想之前的普通函数,必须一次性运行完毕,生成器函数由于有了f_lasti就知道下一次运行应该从什么位置开始
'''
25 0 LOAD_CONST 1 (1)
2 YIELD_VALUE
4 POP_TOP
26 6 LOAD_CONST 2 ('Tom-1')
8 STORE_FAST 0 (name)
27 10 LOAD_CONST 3 (2)
12 YIELD_VALUE
14 POP_TOP
28 16 LOAD_CONST 4 (18)
18 STORE_FAST 1 (age)
29 20 LOAD_CONST 5 ('ABC')
22 RETURN_VALUE
None
-1
{}
2
{}
12
{'name': 'Tom-1'}