2.6 Python类的创建与调用:从代码对象到PVM执行

传统教学中“类像蓝图,对象像实例”的类比,掩盖了Python动态面向对象的本质。本文将从Python代码编译结构(Code Object)、运行时对象模型(Type/Class)、PVM执行机制三个层面,揭示类从定义到调用的完整生命周期。

一、类的定义 - 静态编译阶段的代码对象生成

在 Python 中,当解释器遇到class关键字定义的类时,整个过程会涉及静态编译阶段和动态执行阶段的协同工作,而代码对象的生成是静态编译阶段的核心环节。从源代码到类对象的创建,首先需要经过词法分析和语法分析,将类定义的源代码转换为抽象语法树(AST)。例如,当代码中出现class MyClass: pass这样的定义时,语法分析器会识别出类定义的结构,包括类名、基类以及类体中的语句。

接下来,编译器会针对类体中的代码生成对应的代码对象(code object)。代码对象是一种包含字节码指令、常量表、变量名称表等元数据的结构化对象,它是 Python 虚拟机执行的基础。在静态编译阶段,编译器并不会执行类体中的代码,而是分析类体的语法结构,将其转换为字节码指令序列,存储在代码对象的co_code属性中。同时,类体中引用的常量(如字符串、数字)会被收集到代码对象的常量表co_consts中,变量名会被记录在代码对象的变量名称表co_namesco_varnames中。

我们首先新建一个名为target.py的Python文件,在文件中写入下面的代码用于测试:

class Student:
    teacher='Teacher Wang'
    def __init__(self, name):
        self.name = name

    def say_hello(self):
        print(f"Hello, I'm {self.name}.")

    def say_teacher(self):
        print(f"Our teacher is {self.teacher}.")

xiao_ming=Student('Xiao Ming')

在静态编译阶段,编译器会将target.py编译为5个代码对象,它们分别是模块、Student类、__init__方法、say_hello方法和say_teacher方法的代码对象。我们可以运行下面的脚本查看反汇编后的字节码和各个代码对象信息,注意该脚本应和target.py同目录。我们暂时只要关注代码对象的常量表、全局名称表和局部名称表,这些表是PVM实际执行的核心数据之一。

import dis
import inspect
import target

def analysis():
    save_path = 'analysis_result.txt'
    file = open(save_path, 'w', encoding='utf-8')
    code = inspect.getsource(target)
    code_obj_module = compile(code, filename="", mode="exec")
    print("反汇编后的字节码:", file=file)
    dis.dis(code_obj_module, file=file)

    print('\n模块代码对象信息:', file=file)
    print("\t常量表:", code_obj_module.co_consts, file=file)
    print("\t全局名称表:", code_obj_module.co_names, file=file)
    print("\t局部名称表:", code_obj_module.co_varnames, file=file)
    print("\t实际字节码(十六进制):", code_obj_module.co_code.hex(), file=file)

    code_obj_Student=code_obj_module.co_consts[0]
    print('\nStudent类的代码对象信息:', file=file)
    print("\t常量表:", code_obj_Student.co_consts, file=file)
    print("\t全局名称表:", code_obj_Student.co_names, file=file)
    print("\t局部名称表:", code_obj_Student.co_varnames, file=file)
    print("\t实际字节码(十六进制):", code_obj_Student.co_code.hex(), file=file)

    code_obj_init=code_obj_Student.co_consts[2]
    print('\nStudent.__init__方法的代码对象信息:', file=file)
    print("\t常量表:", code_obj_init.co_consts, file=file)
    print("\t全局名称表:", code_obj_init.co_names, file=file)
    print("\t局部名称表:", code_obj_init.co_varnames, file=file)
    print("\t实际字节码(十六进制):", code_obj_init.co_code.hex(), file=file)

    code_obj_hello = code_obj_Student.co_consts[4]
    print('\nStudent.say_hello方法的代码对象信息:', file=file)
    print("\t常量表:", code_obj_hello.co_consts, file=file)
    print("\t全局名称表:", code_obj_hello.co_names, file=file)
    print("\t局部名称表:", code_obj_hello.co_varnames, file=file)
    print("\t实际字节码(十六进制):", code_obj_hello.co_code.hex(), file=file)

    code_obj_teacher = code_obj_Student.co_consts[6]
    print('\nStudent.say_teacher方法的代码对象信息:', file=file)
    print("\t常量表:", code_obj_teacher.co_consts, file=file)
    print("\t全局名称表:", code_obj_teacher.co_names, file=file)
    print("\t局部名称表:", code_obj_teacher.co_varnames, file=file)
    print("\t实际字节码(十六进制):", code_obj_teacher.co_code.hex(), file=file)


if __name__ == '__main__':
    analysis()

值得注意的是,静态编译阶段生成的代码对象并不包含类的实例化逻辑,而是为动态执行阶段做准备。当程序运行到类定义语句时,Python 虚拟机会动态执行该代码对象中的字节码,创建类的命名空间,执行类体中的赋值操作,并将方法函数对象绑定到类属性上。因此,代码对象是连接静态编译和动态执行的桥梁,它封装了类定义的逻辑结构,使得 Python 能够在运行时高效地构建类对象及其属性。

静态编译阶段的代码对象生成过程,本质上是将人类可读的类定义源代码转换为计算机可执行的字节码指令结构,这个过程不涉及实际的变量赋值或函数调用,而是完成语法验证和结构组织,为后续类对象的创建和实例化奠定基础。代码对象的存在使得 Python 既能保持动态语言的灵活性,又能通过预编译优化执行效率。

二、类的创建 - 运行时类对象的动态构建

前面的文章已经介绍过,在Python程序启动时,PVM首先进入初始化阶段,初始化过程首先会创建主模块的全局命名空间,然后将编译后的代码对象加载到内存,之后创建初始栈帧(包含代码对象、全局命名空间等),最后初始化指令指针。此时,程序正式进入动态执行阶段,在上面的示例中,编译器会将target.py编译为5个代码对象,其中target模块代码对象是PVM执行的一个代码对象。相应的,PVM根据模块代码对象创建第一个栈帧并开始执行,具体执行的字节码如下,我们对每个字节码指令加了注释。

首先通过LOAD_BUILD_CLASS加载类构建机制,接着从常量池读取 Student 类的代码对象和类名'Student',用MAKE_FUNCTION将代码对象转为函数,再调用该函数并传入类名来创建类对象,最后将类存储到全局变量Student中。这些指令是类的创建过程,该过程涉及比较复杂的底层机制,最后将定义好的类保存到全局命名空间中。

 1           0 LOAD_BUILD_CLASS          # 加载内置的类构建函数
              2 LOAD_CONST               0 (", line 1>)
                                      # 加载Student类的代码对象
              4 LOAD_CONST               1 ('Student') # 加载类名'Student'
              6 MAKE_FUNCTION            0 # 创建类构造函数
              8 LOAD_CONST               1 ('Student') # 再次加载类名
             10 CALL_FUNCTION            2 # 调用函数创建Student类
             12 STORE_NAME               0 (Student) # 将类存储到全局命名空间

从上面的字节码可以看出,类的创建始于LOAD_BUILD_CLASS指令,该指令加载了内置的__build_class__函数。这个函数是类构造的起点,负责协调元类(一个用于创建其他类的类对象)的调用、命名空间的初始化以及最终的类实例化。__build_class__函数由Python解释器内部实现,它接收三个关键参数:类体函数、类名和基类元组。虽然字节码中没有显式出现基类元组,但默认情况下会继承object类。

接下来,LOAD_CONST指令加载了常量表中的代码对象。这个代码对象是在编译阶段生成的,包含了类体内的所有逻辑,比如__init__方法定义、类变量赋值等。代码对象由Python编译器在解析class语句时生成,并存储在.pyc文件中。代码对象的数据结构包括co_consts、co_names和co_code等字段,分别用于存储类内常量、变量名集合和实际的字节码指令序列。这个代码对象随后会在MAKE_FUNCTION指令中被包装成一个可调用的类体函数。注意在Python中,不管代码对象来自于哪里(函数、类、模块),只要字节码有效,就可以封装为函数。

类体函数的作用是动态构建类的命名空间,具体执行的字节码为上述的Student类代码对象的字节码。当这个函数被调用时,它会创建一个临时字典作为初始命名空间,然后在这个字典的上下文中执行类体内的所有语句。在这个过程中,方法定义会创建函数对象并绑定到字典中,类变量则会直接以键值对的形式存入字典。最终,这个填充完毕的命名空间字典会被返回,供后续的类构造过程使用。

随后,字节码再次加载类名字符串,并通过CALL_FUNCTION指令调用__build_class__函数。这个调用实际上将控制权移交给了元类系统,元类是一个用于创建类对象的类,它会为类的创建分配内存,并初始化类对象。build_class__首先会确定要使用的元类,检查基类是否定义了__classcell,或者用户是否显式指定了元类。如果没有显式指定,则默认使用type作为元类。确定元类后,__build_class__会调用元类的__prepare__方法来获取初始的命名空间容器。默认情况下,type.__prepare__返回一个普通字典,但这个行为可以被自定义元类重写,比如返回一个有序字典以保持属性定义的顺序。

有了初始化的命名空间容器后,__build_class__会调用之前创建的类体函数,将命名空间作为参数传入。类体函数在这个命名空间中执行类体内的所有代码,填充方法、类变量等内容。执行完毕后,__build_class__会调用元类的__new__方法来实际创建类对象。这个阶段是类构造的核心,元类的__new__方法负责分配内存创建类对象,解析方法装饰器,创建类属性描述符,以及构建方法解析顺序(MRO)。最终,__new__方法返回的类对象会被存储在全局命名空间中,完成整个类的创建过程。

可以看到这个过程涉及大量底层的复杂操作,主要包括元类的概念、MRO的构建、对不同类型方法(如静态方法、装饰器方法)的处理等,以及闭包的处理,这些内容我们将在后面的文章讨论。

三、类的实例化

创建好类对象并将类名保存到全局命名空间后,我们就可以对类进行实例化,具体字节码如下。在Python的底层实现中,实例创建过程展现了一套精密的运作机制。当解释器执行到加载Student类的指令时,它会按照LEGB规则逐层搜索命名空间,这个搜索过程从局部作用域开始,逐步扩展到闭包、全局直至内置命名空间。类对象在内存中以一个复杂的数据结构存在,不仅包含存储属性和方法的__dict__,还维护着弱引用支持、文档字符串和方法解析顺序等关键信息。

 12          14 LOAD_NAME                0 (Student) # 加载Student类
             16 LOAD_CONST               2 ('Xiao Ming') # 加载初始化参数'Xiao Ming'
             18 CALL_FUNCTION            1 # 调用类构造函数创建实例
             20 STORE_NAME               1 (xiao_ming) # 将实例存储为xiao_ming
             22 LOAD_CONST               3 (None) # 加载None
             24 RETURN_VALUE             # 返回None

紧接从常量表中加载初始化参数,然后调用函数,此时Python启动了一个多阶段的实例创建流程。首先进行严格的类型检查,验证被调用的Student类确实实现了__call__方法,并确保参数数量与类定义相符。实际上,Student类本身是type元类的实例,调用类对象首先触发的是type.__call__方法。这是Python类实例化的总控开关,它协调整个对象创建流程。type.__call__方法会为实例分配内存空间,并设置包含引用计数和类型指针在内的对象头信息。这个阶段充分体现了Python内存管理的精细设计。type.__call__方法可以简化的表示为下面的形式,它会调用类的__new__方法分配内存并创建实例,注意不是元类的__new__方法,元类的__new__方法用于创建类对象。之后调用类的__init__方法初始化类的属性。最后,PVM将实例保存到命名空间中,解释器会根据当前执行环境决定更新局部作用域还是全局作用域。

def __call__(cls, *args, **kwargs):
    # 1. 调用类的__new__创建实例
    instance = cls.__new__(cls, *args, **kwargs)
    
    # 2. 如果返回的是该类实例,则初始化
    if isinstance(instance, cls):
        instance.__init__(*args, **kwargs)
    
    return instance

与类创建过程相比,实例化过程虽然同样复杂,但元类的介入程度明显降低。实例化主要依赖类的__new__和__init__方法,而不是像类创建那样完全由元类控制。这种差异体现了Python对不同层次对象创建过程的有意区分,既保证了灵活性,又维持了合理的抽象边界。

整个实例创建机制处处体现着Python的设计哲学。类调用与函数调用使用相同的指令,体现了语言的一致性;将内存分配与初始化分离展现了清晰的抽象层次;而通过__new__和__call__实现的定制能力则彰显了对开发者扩展性的重视。这种设计在保持高度动态性的同时,通过精心优化的底层实现,提供了令人满意的运行时性能。

四、总结

本文深入剖析了Python类的生命周期,揭示其从定义到调用的完整过程。首先通过静态编译阶段将类定义转换为包含字节码的代码对象,为动态执行奠定基础。在运行时,通过__build_class__函数和元类机制动态构建类对象,包括命名空间初始化、方法解析顺序构建等核心环节。最后在实例化阶段,通过type.__call__协调内存分配和初始化,完成对象创建。整个过程体现了Python既保持动态语言灵活性,又通过预编译优化执行效率的设计哲学,展现了类与元类系统的精妙协作机制。

你可能感兴趣的:(python学习资料,python,开发语言,算法)