硬核!Python 四种变量的代码对象和反汇编分析

b320ddc7bafa3025c18e7a4926d3c3b0.gif

作者 | 大奎

整理 | 阳哥

来源丨Python数据之道

在Python基础的学习过程中,对变量和参数的理解有助于我们从更基础层面了解Python语言的运行。在这个过程中,还是有不少冷门和细节的地方需要进一步熟悉。今天我们来分享Python四种变量的代码对象和反汇编分析~

本文我们查看代码对象属性,可以清楚认识变量的分类。反汇编代码,可以弄清各变量在字节码处理上的不同。

代码对象

每个函数都有代码对象(code  object): code,这个对象中存储了函数的字节码和编译信息。其中,以 co_ 开头的属性,是本文我们需要关注的。

以下代码中,我们建立了内置变量 builtins.b、全局变量 g、函数 outer 中新建了局部变量 o1、 o2,还有自由变量 e。内嵌函数 inner 新建局部变量 i1、 i2,引用外部变量 o1、 o2、 e,引用了外部全局变量 g 和内置变量 builtins.b。之后 outer 函数 和  inner 嵌入函数的代码对象:

import builtins

builtins.b = 'builtins' 
g = 'global'

vars = 'argcount cellvars consts filename firstlineno flags freevars lnotab name names nlocals stacksize varnames'.split()

def outer(o1,o2='o2'):
    e = 'enclose'
    def inner(i1,i2='i2'):
        print(i1,i2,o1,o2,e,g,b)
    return inner 

fun = outer('o1')

for var in vars:
    s = eval('outer.__code__.co_%s'%var)
    s2 = eval('fun.__code__.co_%s'%var) 
    print('%15s:%s|%s'%(var,s,s2)) 

fun('i1')

可得输出结果。

argcount:2|2
       cellvars:('e', 'o1', 'o2')|()
       consts:(None, 'enclose', 'i2', <code object inner at 0x004F5D88, file "scope_of_variable.py", line 10>, 'outer.<locals>.inner', ('i2',))|(None,)
       filename:scope_of_variable.py|scope_of_variable.py
    firstlineno:8|10
          flags:3|19
       freevars:()|('e', 'o1', 'o2')
         lnotab:b'\x00\x01\x04\x01\x12\x02'|b'\x00\x01'
           name:outer|inner
          names:()|('print', 'g', 'b')
        nlocals:3|2
      stacksize:4|8
       varnames:('o1', 'o2', 'inner')|('i1', 'i2')
i1 i2 o1 o2 enclose global builtins

根据以下字段意义,可进一步确认内省函数中变量的分类和作用域。

  • co_argcount 函数形式参数个数

  • co_cellvars 被内部包含函数引用的变量组成的元组。也就是所谓 自由变量

  • co_freevars 当前函数 引用的外部自由变量 组成的元组,和上个 co_cellvars 是相对的概念

  • co_consts 常量,包括如下:

  • None 函数返回值,系统自带

  • 从前往后数所有字面常量

  • 内嵌函数代码对象 code  object

  • 内嵌函数的 qual_name 常量,如:outer..inner

  • co_names 本函数用到的 全局变量、系统内置变量

  • co_nlocals 本函数的局部变量个数

  • co_varnames 本函数 局部变量 ,不含被引用自由变量,包含形式参数和内嵌函数绑定变量

上文中的常量、局部变量、全局变量分别存储在不同的元组中,在代码编译为字节码时,使用不同指令来取得、修改。自由变量的处理则更为复杂一些。

使用反汇编可以一探其中奥秘。

反汇编代码

编程语言分为编译型和解释型。编译型如 C 语言,预先 gcc 成.exe 文件。解释型如 PHP ,在执行时才一条条顺序读入,顺序解释执行。Python 则采用编译成中间代码,再虚拟机运行的方式。编译的中间代码称为字节码,虚拟机每个平台是不同的,虚拟机负责将字节码转义为对应平台的二进制指令。这点类似于 Java 的字节码,所以两者都具有平台无关性:一处编码,处处运行。原理就在这里。

Python 中的 dis 模块,可以把程序反汇编为字节码。Python 字节码使用 LOAD_FAST 0这种形式,前面是指令 LOAD_FAST,后面是参数 0。整条语句就是:”把局部变量元组 0 处变量,加载到当前栈顶。“

这里的局部变量元组是上文的 co_varnames。

这里的当前栈顶表示的是当前帧的计算栈。CPython 虚拟机是基于帧栈的机器,也就是说 CPython 虚拟机运行在一个栈里。帧栈里是每次函数调用形成的帧,而每个帧里面是计算栈和块栈。计算栈是函数主体运行的部分,块栈是程序中 with、 try、循环执行的地方。

使用如下代码,来反汇编 outer 和 inner 函数。

import dis 
print(dis.dis(outer()))

这是反汇编 outer() 执行结果,也就是 inner 函数本身。

可得结果如下。首先加载全局变量元组第 0 个元素 print 函数。然后加载局部变量 i1、 i2,之后加载自由变量 o1、 o2、 e,再加载全局变量 e 和 g,调用函数 print 输出结果。结果弹栈,加载 None,返回结果。

12           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (i1)
              4 LOAD_FAST                1 (i2)
              6 LOAD_DEREF               1 (o1)
              8 LOAD_DEREF               2 (o2)
             10 LOAD_DEREF               0 (e)
             12 LOAD_GLOBAL              1 (g)
             14 LOAD_GLOBAL              2 (b)
             16 CALL_FUNCTION            7
             18 POP_TOP
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE

以及print(dis.dis(outer))可得结果。

9           0 LOAD_CONST               1 ('enclose')
              2 STORE_DEREF              0 (e)

 11           4 LOAD_CONST               5 (('i2',))
              6 LOAD_CLOSURE             0 (e)
              8 LOAD_CLOSURE             1 (o1)
             10 LOAD_CLOSURE             2 (o2)
             12 BUILD_TUPLE              3
             14 LOAD_CONST               3 (<code object inner at 0x00225D88, file "scope_of_variable_bytecode.py", line 11>)
             16 LOAD_CONST               4 ('outer.<locals>.inner')
             18 MAKE_FUNCTION            9 (defaults, closure)
             20 STORE_FAST               2 (inner)

 13          22 LOAD_FAST                2 (inner)
             24 RETURN_VALUE

Disassembly of <code object inner at 0x00225D88, file "scope_of_variable_bytecode.py", line 11>:
 12           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (i1)
              4 LOAD_FAST                1 (i2)
              6 LOAD_DEREF               1 (o1)
              8 LOAD_DEREF               2 (o2)
             10 LOAD_DEREF               0 (e)
             12 LOAD_GLOBAL              1 (g)
             14 LOAD_GLOBAL              2 (b)
             16 CALL_FUNCTION            7
             18 POP_TOP
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE
None

以上代码中。- 第 9 行,加载常量并且存储为自由变量。- 第 11 行,加载常量作默认值、加载自由变量构成元组,以构成闭包参数,然后编译内部函数 inner 字节码,使用默认值和闭包做参数构建内部函数 inner。- 第 13 行,返回函数 inner。

这也是闭包的原理,闭包中的自由变量 i1、 i2、 e 和函数 inner 构成了一个整体代码块,自由变量附着在 inner 函数上,形成了一个特殊作用域。

以上所有指令,都定义在 include/opcode.h 中,它指代的整数在 Python 程序的主循环 ceval.c 中分别判断,然后拆解成 C 语言指令执行。可以参考 CPython 的 ceval 源代码

https://github.com/python/cpython/blob/b11a951f16f0603d98de24fee5c023df83ea552c/Python/ceval.c

总结

Python 中的局部、全局、内置、自由变量四种类型变量在作用域和使用上各不相同,使用代码对象,反汇编可以更清楚认识变量情况,为理解闭包、装饰器打下基础。

15a999823f14447d758d8df727c718cd.gif

技术

用Python写一个天天酷跑

资讯

Nginx宣布在俄罗斯禁止贡献

技术

学会用Opencv做贪吃蛇游戏

技术

一行Python代码能干嘛?来看!

3a9df099f2436fa612e3f7777150a23c.png

分享

34dca51fd68f21f309dd83509b707fec.png

点收藏

a97143f0372abf9fa21f59c299299356.png

点点赞

38fbc845e5fc7b8ca0b6961163b878eb.png

点在看

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值