Python函数背后的秘密(2)

函数对象是如何创建的?#

程序运行时,虚拟机看见函数定义 def 语句,它都干些啥呢?

还是举一个简单例子吧,我们将一段代码放在字符串 s 中,然后用编译函数将它编译为字节码,再进行反汇编,就可以看到它的汇编指令了。

import dis

s = """
def foo(x,y='默认值',*,z='仅限关键字'):
    a = 1
"""

c = compile(s, "func", "exec")
dis.dis(c)

下面是用 Python3.10.1 运行的结果(不同版本的结果可能有差异):

  2           0 LOAD_CONST               6 (('默认值',))
              2 LOAD_CONST               1 ('仅限关键字')
              4 LOAD_CONST               2 (('z',))
              6 BUILD_CONST_KEY_MAP      1
              8 LOAD_CONST               3 (<code object foo ...)
             10 LOAD_CONST               4 ('foo')
             12 MAKE_FUNCTION            3 (defaults, kwdefaults)
             14 STORE_NAME               0 (foo)
             16 LOAD_CONST               5 (None)
             18 RETURN_VALUE

Disassembly of <code object foo at 0x00000263A4993940, file "func", line 2>:
  3           0 LOAD_CONST               1 (1)
              2 STORE_FAST               3 (a)
              4 LOAD_CONST               0 (None)
              6 RETURN_VALUE

反汇编后有两个代码对象,第一个代码对象是 s 编译后得到的模块级的代码对象 c,第二个代码对象是函数 foo 的代码对象。def 语句位于模块中,函数对象是在模块中创建的。

c.co_consts 属性指令会用到,我们来看看其样子:

from pprint import pprint
pprint(c.co_consts)

('默认值',
 '仅限关键字',
 ('z',),
 <code object foo at 0x00000263A4993940, file "func", line 2>,
 'foo',
 None,
 ('默认值',))

对于模块级的代码对象 c 来说,co_names 就是其局部变量名组成的元组:(‘foo’,)。

列出了这些属性,就比较容易看懂创建函数的这些个指令了:

LOAD_CONST 6 ((‘默认值’,))
把 co_consts [6] 即元组 (‘默认值’,) 压入栈

LOAD_CONST 1 (‘仅限关键字’)
把 co_consts [1] 即’仅限关键字’压入栈

LOAD_CONST 2 ((‘z’,))
把 co_consts [2] 即键元组 (‘z’,) 压入栈

BUILD_CONST_KEY_MAP 1
将栈顶的前两项弹出,构建一个字典并压入栈

LOAD_CONST 3 (<code object foo …>)
把 c.co_consts [6], 即函数 foo 的代码块对象压入栈

LOAD_CONST 4 (‘foo’)
把 c.co_consts [7],即函数名 foo 压入栈

MAKE_FUNCTION 3 (defaults, kwdefaults)
这条指令创建函数对象,看来秘密在这里:

  • 弹出栈中的函数名。
  • 弹出对应的 foo 代码块。
  • 创建函数对象,创建时把全局名字空间传递进去:
    • 为函数对象申请空间,创建函数对象。
    • 然后设置函数对象的成员属性。

MAKE_FUNCTION 后面的标志(本指令为 3 即 00000011),表示在栈帧中,代码对象 foo 的下面依次存储的是:

  • 仅限关键字构成的字典。
  • 默认值元组,位置参数和仅限位置参数的默认值,调用时可以用位置实参、关键字实参值来覆盖它们。

经过上面这些指令,把函数参数封装进了函数对象中了。后面两句是例行的返回指令。

                           未完待续
本作品采用《CC 协议》,转载必须注明作者和本文链接