zoukankan      html  css  js  c++  java
  • python函数的执行过程

    对于 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'dx01}x00tx00jx01x83x00ax02tx03tx02jx04x83x01x01x00tx03dx02x83x01x01x00|x00Sx00'  
    
    >>> 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/

  • 相关阅读:
    return false 和 return true
    前端异常采集
    Performance — 前端性能监控利器
    正则笔记-忘记就来看
    JS中的正则应用
    iconfont的使用
    canvas笔记1
    树莓派3B安装DeBian 64位系统及其安装私有云网盘----可道云
    台灯改造【智能台灯】
    单片机C语言中标志位的经典应用
  • 原文地址:https://www.cnblogs.com/mrwuzs/p/11986390.html
Copyright © 2011-2022 走看看