# 类机制与字节码
# compile 手动编译类
# compile(text,'','exec')
>>> text = '''
... class Dog:
... def yelp(self):
... print('woof')
... '''
>>> code = compile(text, '', 'exec')
>>> import dis
>>> dis.dis(code)
2 0 LOAD_BUILD_CLASS
2 LOAD_CONST 0 (<code object Dog at 0x10110f8a0, file "", line 2>)
4 LOAD_CONST 1 ('Dog')
6 MAKE_FUNCTION 0
8 LOAD_CONST 1 ('Dog')
10 CALL_FUNCTION 2
12 STORE_NAME 0 (Dog)
14 LOAD_CONST 2 (None)
16 RETURN_VALUE
Disassembly of <code object Dog at 0x10110f8a0, file "", line 2>:
2 0 LOAD_NAME 0 (__name__)
2 STORE_NAME 1 (__module__)
4 LOAD_CONST 0 ('Dog')
6 STORE_NAME 2 (__qualname__)
3 8 LOAD_CONST 1 (<code object yelp at 0x10110f390, file "", line 3>)
10 LOAD_CONST 2 ('Dog.yelp')
12 MAKE_FUNCTION 0
14 STORE_NAME 3 (yelp)
16 LOAD_CONST 3 (None)
18 RETURN_VALUE
Disassembly of <code object yelp at 0x10110f390, file "", line 3>:
4 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('woof')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
# 虚拟机执行过程
# 创建名为 Dog 的函数对象
以上面手动编译的 Dog 类为例。
模块代码执行时,虚拟机内部有一个 栈帧 对象,维护着代码对象运行时时的上下文信息,全局和局部名字空间均指向模块属性空间。
前 3 条字节码,负责往运行栈加载数据。LOAD_BUILD_CLASS 这个字节码源码在 Python/ceval.c 中,其作用很简单,只是将 __build_class__ 这个内建函数加载进栈顶。该函数为 builtins 模块中的一员,一个负责创建类的工具函数。
接着,第 2 条字节码将下标为 0 的常量加载到栈顶,这是代表 Dog 类代码块的代码对象,如红色箭头所示。第 3 条字节码将常量 Dog 加载到栈顶。第 4 条字码时我们在函数机制中学过的 MAKE_FUNCTION指令,用于创建函数对象。
# 名为 Dog 的函数对象创建
指令执行完毕后,名为 Dog 的函数对象就被保存在栈顶了。
# __build_class__
# 执行前
查看 __build_class__ 函数帮助文档可以得知,该函数第一个参数是一个函数,第二个参数是一个名字。
>>> help(__build_class__)
Help on built-in function __build_class__ in module builtins:
__build_class__(...)
__build_class__(func, name, *bases, metaclass=None, **kwds) -> class
Internal helper function used by the class statement.
其中第一个函数由字节码 2-4 创建完成,即上一步创建的名为 Dog 的函数,第二个名字将在字节码 5 处加载入栈帧对象的临时栈顶。
__build_class__源码实现在Python/bltinmodule.c,观察可以发现,其并没有直接调用新创建的Dog函数,而是先创建一个dict对象,作为新生类的属性空间,然后从Dog函数中取出全局名字空间和代码对象,最后新建一个栈帧对象并开始执行Dog类代码对象。
其中关键的关系如下:
模块的属性空间=Dog函数的全局名字空间=栈帧对象的全局名字空间
栈帧对象的f_global会继承父栈帧对象的f_global
Dog函数的代码对象=Dog类的代码对象
Dog函数的局部名字空间(
__dict__)=Dog类的属性空间
即实际上Dog函数只是作为打包参数的包袱,将代码对象、全局名字空间等参数作为一个整体进行传递
# 准备属性空间
代码先从全局名字空间取出模块名 __name__,在局部名字空间中保存为 __module__,然后将类名 Dog 保存到局部名字空间中的 __qualname__。最后取出 yelp 函数的代码对象,完成 yelp 函数对象的创建工作。至此,新生类 Dog 的属性空间完成初始化,属性空间准备完成。
# 创建新生类
类属性空间准备完毕,__build_class__接着调用 type 元类型对象完成 Dog 类的创建。type 需要的参数有 3个,分别是: 类名 、 基类列表 (类继承)以及代表 类属性空间 的 dict 对象。
至此,Dog类创建完成
>>> help(type)
Help on class type in module builtins:
class type(object)
| type(object_or_name, bases, dict)
| type(object) -> the object's type
| type(name, bases, dict) -> a new type
# 收尾
模块代码对象最后的字节码将 Dog 类对象保存于模块属性空间
# Python模拟
# 模块代码对象
>>> code
<code object <module> at 0x10b42e8a0, file "", line 2>
# Dog 类代码对象
>>> code.co_consts[0]
<code object Dog at 0x10b42ac00, file "", line 2>
# Dog 类 yelp 函数代码对象
>>> code.co_consts[0].co_consts[1]
<code object yelp at 0x10b5136f0, file "", line 3>
# 新建一个dict对象作为类的属性空间
>>> attrs = {}
# 以类属性空间为局部名字空间,执行类代码对象,以此完成新类属性空间初始化
>>> exec(code.co_consts[0], globals(), attrs)
>>> attrs
{'__module__': '__main__', '__qualname__': 'Dog', 'yelp': <function Dog.yelp at 0x10b732e18>}
# 调用type函数完成类对象创建
>>> Dog = type('Dog', (), attrs)
>>> Dog
<class '__main__.Dog'>
# 使用
>>> dog = Dog()
>>> dog.yelp()
woof