对于 Python 常规函数,都只有一个入口,但会有多个出口如 return 返回或者抛出异常。函数从入口进入会一直运行到 return 语句或者抛出异常,中间不会暂停,函数一直拥有控制权。当运行结束,才将控制权还给调用者。

前文介绍过,当执行 Python 代码时,会先将代码编译成字节码,然后在虚拟机中解释执行字节码,编译好的字节码会保存在 .pyc 或 .pyd 扩展名的文件里。在运行时,虚拟机会创建字节码执行的上下文环境,Python 模拟 C 语言中的运行栈作为运行时的环境,使用PyFrameObject表示运行时的栈,而字节码会存储在 PyCodeObject 对象中。

Python 解释器是基于栈的,其中有三种栈:调用栈 (frame stack)、数据栈 (data stack)、块栈 (block statck)。

PyFrameObject 存在于调用栈,其中字段 f_back 指向上一级 PyFrameObject,这样就形成了一个调用链。每个调用栈对应函数的一次调用。调用栈中会有自己的数据栈和块栈,数据栈中会存放字节码操作的数据,块栈用于特定的控制流块,比如循环和异常处理。

打开终端,在命令行输入 python3ipython 命令打开 Python 命令行交互解释器:

如果使用 ipython 需提前安装,需要在 Python 3 环境下。

pip3 intall ipython
import inspect # 全局变量
a = 0
x, y = None, None def fun1():
b = 1 # 定义局部变量
global x # 将变量 x 设为全局变量,因为它会在函数内部被修改
# inspect 获取当前栈帧,相当于 PyFrameObject 对象
x = inspect.currentframe()
# 打印当前栈帧中运行的行号
print(x.f_lasti)
print('running fun1')
return b def fun2(d):
# 局部变量赋值
c = a
e = d
print('running fun2')
# 调用方法
fun1()
global y
# 获取当前栈帧
y = inspect.currentframe()
f = 2
return f import dis # dis 方法查看函数的字节码
>>> dis.dis(fun2) 2 0 LOAD_GLOBAL 0 (a)
2 STORE_FAST 1 (c) 3 4 LOAD_FAST 0 (d)
6 STORE_FAST 2 (e) 4 8 LOAD_GLOBAL 1 (print)
10 LOAD_CONST 1 ('running fun2')
12 CALL_FUNCTION 1
14 POP_TOP 5 16 LOAD_GLOBAL 2 (fun1)
18 CALL_FUNCTION 0
20 POP_TOP 7 22 LOAD_GLOBAL 3 (inspect)
24 LOAD_ATTR 4 (currentframe)
26 CALL_FUNCTION 0
28 STORE_GLOBAL 5 (y) 8 30 LOAD_CONST 2 (2)
32 STORE_FAST 3 (f)
34 LOAD_CONST 0 (None)
36 RETURN_VALUE

fun2 函数的字节码,每一列分别是:

源码行号 | 指令在函数中的偏移 | 指令符号 | 指令参数 | 实际参数值(参考)

先来了解一下 Python 方法的执行过程。在代码运行时,字节码会存储在 PyCodeObject 对象中。PyCodeObject 保存了编译后的静态信息,在运行时再结合上下文形成一个完整的运行态环境。函数的 code 变量就是指向的 PyCodeObject 对象,可以查看字节码信息。

>>> fun1.__code__.co_code           # 查看字节码
b'd\x01}\x00t\x00j\x01\x83\x00a\x02t\x03t\x02j\x04\x83\x01\x01\x00t\x03d\x02\x83\x01\x01\x00|\x00S\x00' >>> list(fun1.__code__.co_code) # 转换成 list 之后,是由指令符号后面跟着指令参数组成,指令参数根据指令符号不同个数不同
[100, 1, 125, 0, 116, 0, 106, 1, 131, 0, 97, 2, 116, 3, 116, 2, 106, 4, 131, 1, 1, 0, 116, 3, 100, 2, 131, 1, 1, 0, 124, 0, 83, 0] >>> dis.opname[100] # dis 模块的 opname 存放了操作码
'LOAD_CONST' # 100, 1 就是相当于 LOAD_GLOBAL 1 >>> dis.opname[125]
'STORE_FAST' # 125, 0 就是相当于 STORE_FAST 0 # PyCodeObject对象中存放这当前上下文的数据
>>> fun1.__code__.co_varnames # 局部变量名的元组
('b',) >>> fun1.__code__.co_consts # 局部变量中的常量元组
(None, 1, 'running fun1') >>> fun1.__code__.co_names # 名称的元组
('inspect', 'currentframe', 'x', 'print', 'f_lasti') >>> fun2.__code__.co_varnames
('d', 'c', 'e', 'f') >>> fun2.__code__.co_consts
(None, 'running fun2', 2) >>> fun2.__code__.co_names
('a', 'print', 'fun1', 'inspect', 'currentframe', 'y')

co_code 中存储了字节码,字节码使用二进制方式存储,节省存储空间,指令符号是常量对应的,在指令符号后面跟着指令参数,这样便于操作。

  • co_varnames 包含局部变量名的元组,所有当前局部变量
  • co_consts 包含字节码所用字面量的元组,局部常量
  • co_names 包含字节码所用名称的元组

inspect 可以获取调用栈的信息,当执行函数时:

# 运行方法 fun1
# f_lasti 记录当前栈帧中运行的行号
>>> fun1()
16
running fun1
1 # 调用栈中存储了字节码信息
>>> x.f_code == fun1.__code__
True # co_name 是方法名
>>> x.f_code.co_name
'fun1' # f_locals 存放局部变量的值
>>> x.f_locals
{'b': 1} # 上一级调用栈
>>> x.f_back.f_code.co_name
'<module>' # 调用方法 fun2
>>> fun2(6)
running fun2
24
running fun1
2 >>> y.f_code.co_name
'fun2' # 上一级调用栈,fun2 函数调用 fun1 函数,所以 fun1 的上一级调用栈是 fun2
>>> x.f_back.f_code.co_name
'fun2' >>> y.f_code.co_names
('a', 'print', 'fun1', 'inspect', 'currentframe', 'y') >>> y.f_code.co_consts
(None, 'running fun2', 2) # fun2 方法的局部变量
>>> y.f_locals
{'f': 2, 'e': 6, 'c': 0, 'd': 6} # fun2 中的全局变量存放在 f_globals 中,并且包含内置变量
>>> y.f_globals['a']
0

介绍几个常用字节码的意思:

LOAD_GLOBAL 0 (a)

LOAD_GLOBAL 是取 co_names 元组中索引为 0 的值,即 a,再从 f_globals 中查找 a 的值, 将 a 的值压入数据栈栈顶,即将值 0 压入栈顶

STORE_FAST 1 (c)

STORE_FAST 是取 co_names 元组中索引为 1 的值,即 c,取出数据栈栈顶的值,即刚刚压入栈顶的值 0 ,将值存入 f_locals 中对应的 c 值,这样就完成了 a 到 c 的赋值操作,现在是 {'c': 0}

LOAD_FAST 0 (d)

LOAD_FAST 是取 co_varnames 元组中索引为 0 的值,即 d ,在 f_locals 中查找d的值,将 d 的值 6 压入数据栈栈顶

STORE_FAST 2 (e)

STORE_FAST 是取 co_names 元组中索引为 2 的值, 即 e,取出栈顶的值,存入 f_locals 中对应的 e 值,即 {'e': 6}

LOAD_GLOBAL 1 (print)

LOAD_CONST 1 ('running fun2')

CALL_FUNCTION 1

POP_TOP

将 print 和 'running fun2' 依次压入栈顶,CALL_FUNCTION 调用函数,1 是将栈顶的一个数据 ('running fun2') 弹出作为下一个函数调用的参数,然后弹出 print ,调用 print 函数。执行函数 print('running fun2')

LOAD_FAST 3 (f)

RETURN_VALUE

将f的值压入栈顶,RETURN_VALUE 将栈顶的值取出,作为函数返回的值,传给上一级的调用栈,开始运行上一级的调用栈。

Python 中函数执行过程和数据存储是分开的。函数在调用执行时依据调用栈,每个调用栈都有自己的数据栈,数据存放在数据栈中,调用栈是解释器在堆上分配内存,所以在函数执行结束之后,栈帧还存在,数据还保留。在执行 CALL_FUNCTION 调用其他的函数时,栈帧会使用 f_lasti 记录下执行的行号,在函数返回时继续从 f_lasti 处执行。

来自实验楼

https://www.shiyanlou.com/courses/1292/learning/

最新文章

  1. Shiro - 限制并发人数登录与剔除
  2. [记录][python]python爬虫,下载某图片网站的所有图集
  3. iOS版本更新的App提交审核流程
  4. Windows Azure服务
  5. PMP 第一章 引论
  6. ios中webservice报文的拼接
  7. ARP:地址解析协议
  8. Java SE ---流程控制语句
  9. C#系统缓存全解析
  10. HttpRequest
  11. Js Json 互转
  12. Sending HTML Form Data
  13. onload ready
  14. netty和protobuf的使用
  15. 让zepto支持slideup(),slidedown()
  16. ios scrollView代理的用法
  17. iOS.BackgroundTask
  18. (转) DB2 HADR
  19. 在Chrome浏览器中保存的密码有多安全?
  20. 编写简单的maven插件

热门文章

  1. davinci 删除路线站点关系
  2. python+Appium自动化:读取Yaml配置文件
  3. free命令详解-1
  4. 解决安卓app在真机上的无法登录问题
  5. appium+python 【Mac】UI自动化测试封装框架介绍 &lt;三&gt;---脚本的执行
  6. Java生成压缩文件(zip、rar 格式)
  7. Codeforces Round #588 (Div. 2) D. Marcin and Training Camp(思维)
  8. Acwing-287-积蓄程度(树上DP, 换根)
  9. Codeforces Round #456 (Div. 2) 912E E. Prime Gift
  10. MySql大小写配置