python函数的执行过程
对于 Python 常规函数,都只有一个入口,但会有多个出口如 return 返回或者抛出异常。函数从入口进入会一直运行到 return 语句或者抛出异常,中间不会暂停,函数一直拥有控制权。当运行结束,才将控制权还给调用者。
前文介绍过,当执行 Python 代码时,会先将代码编译成字节码,然后在虚拟机中解释执行字节码,编译好的字节码会保存在 .pyc 或 .pyd 扩展名的文件里。在运行时,虚拟机会创建字节码执行的上下文环境,Python 模拟 C 语言中的运行栈作为运行时的环境,使用PyFrameObject表示运行时的栈,而字节码会存储在 PyCodeObject 对象中。
Python 解释器是基于栈的,其中有三种栈:调用栈 (frame stack)、数据栈 (data stack)、块栈 (block statck)。
PyFrameObject 存在于调用栈,其中字段 f_back 指向上一级 PyFrameObject,这样就形成了一个调用链。每个调用栈对应函数的一次调用。调用栈中会有自己的数据栈和块栈,数据栈中会存放字节码操作的数据,块栈用于特定的控制流块,比如循环和异常处理。
打开终端,在命令行输入 python3
或 ipython
命令打开 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/
python函数的执行过程的更多相关文章
- Python程序的执行过程原理(解释型语言和编译型语言)
Python是一门解释型语言?我初学Python时,听到的关于Python的第一句话就是Python是一门解释型语言,我就这样一直相信下去,直到发现.pyc文件的存在,如果真是解释型语言,那么生成的. ...
- Python程序的执行过程 解释型语言和编译型语言
转载地址:http://blog.csdn.net/lujiandong1/article/details/50067655 1. Python是一门解释型语言? 我初学Python时,听到的关于Py ...
- 说说Python程序的执行过程
1. Python是一门解释型语言? 我初学Python时,听到的关于Python的第一句话就是,Python是一门解释性语言,我就这样一直相信下去,直到发现了*.pyc文件的存在.如果是解释型语言, ...
- Python程序的执行过程
1. Python是一门解释型语言? 我初学Python时,听到的关于Python的第一句话就是,Python是一门解释性语言,我就这样一直相信下去,直到发现了*.pyc文件的存在.如果是解释型语言, ...
- 从底层简析Python程序的执行过程
摘要:是否想在Python解释器的内部晃悠一圈?是不是想实现一个Python代码执行的追踪器?没有基础?不要怕,这篇文章让你初窥Python底层的奥妙. [编者按]下面博文将带你创建一个字节码级别的追 ...
- Python函数之面向过程编程
一.解释 面向过程:核心是过程二字,过程即解决问题的步骤,基于面向过程去设计程序就像是在设计,流水线式的编程思想,在设计程序时,需要把整个流程设计出来, 一条工业流水线,是一种机械式的思维方式 二.优 ...
- python递归函数的执行过程
举例: def nove(n,a,b,c): if n == 1: print(a,'------------>',c) else: nove(n-1,a,c,b) nove(1,a,b,c) ...
- JavaScript 函数的执行过程
每一个JavaScript函数都是Function对象的一个实例, 它有一个仅供JavaScript引擎存取的内部属性[[Scope]]. 这个[[Scope]]存储着一个作用域的集合, 这个集合就叫 ...
- python源码探秘:用户函数的执行过程
脚本函数编译后如何执行?脚本编译后是pyc码,pycodeobject对象的串行化.import时是对pyc文件反系列化.函数编译后会生成函数对象,函数对象的TP_call对应的是function_c ...
随机推荐
- 多进程编程——理论讲解与 multiprocessing 模块
多进程编程 import os pid = os .fork() 功能 :创建新的进程 参数: 无 返回值 :失败返回一个负数 成功:在原有进程中返回新的进程的PID号 在新进程中返回为0* 子进程会 ...
- spring 整合mongodb报NoSuchMethodError错误
刚开始通过网上查到相关的资料进行了一些配置,参考链接:http://www.open-open.com/lib/view/open1454374782167.html maven的dependenci ...
- 关于pycharm database查看db.sqlites文件提示:Driver class 'org.sqlite.JDBC' not found
系统重新安装后,启动pycharm存在各种问题,其中一个问题就是在Pycharm中的database里面不能查看sqlite数据库了: 经过一番查找终于找到了问题: 首先问题 是提示这样一个报错: 解 ...
- 误删rpm命令的恢复方法
rpm命令不能用了,被依赖的yum也不能使用了, 恢复rpm命令无外乎重装, 重装方法1: 使用源码编译, 需要gcc ,cmake包,如果没装,悲剧了 重装方法2: 找一台,和出问题的这台同样系统 ...
- Invalid argument: Key: label. Data types don't match. Data type: int64 but expected type: float
改为
- 微信小程序实现连接蓝牙设备跑步APP
背景 微信小程序兴起,有变成超级APP的趋势,通过微信提供的小程序api,可以通过微信调用到手机原生的支持. 目标 通过微信小程序实现来实现跑步类App的功能. 需求分析 跑步类App需要的两个核心的 ...
- Vue学习日记(三)——Vue路由管理vue-router
前言 为了给读者更好的体验,去理解vue-router和下一篇介绍vuex,决定还是来一个实战教程来带大家更加的去深入理解vue-router,在这之前,读者先自行了解和去官网下载npm和node,附 ...
- mysql5.7备份
一.备份准备&备份测试 1.备份目录准备 #mysql专用目录 mkdir /mysql #mysql备份目录 mkdir /mysql/backup #mysql备份脚本 mkdir /my ...
- javascript---查找节点
快捷键: chazhaojiedian(查找节点) chuangjianjiedian(创建节点) 使用childNodes childElementCount//=====以下是第一种操作D ...
- windows 查看端口占用和杀死进程
在windows命令行窗口下执行:C:\>netstat -aon|findstr "3306" 如上图,端口被进程号为5056的进程占用,继续执行下面命令:C:\>t ...