Python的加载过程与内存模型

Python的加载过程与内存模型(CPython实现)

Python的加载过程

image

编译时

Python 与其他语言一样也具有编译过程 ,将Python文件转换为字节码文件供解释器进行下一步的处理。进行语义分析,语法分析,具象生成树,抽象生成树,最终将文件转换为字节码。

对对象的预处理:

对于左值(变量名)会在编译期被加入名称表(co_names),并加入到 一个名称表元组中。

对于右侧创建的变量,根据类型的不同,加入到常量表中。

将全局变量替换为名称表中的索引,而索引映射了对应的常量对象指针

在python文件中的变量声明”a = 3” 变成了 “名字表中‘a’的内存地址 指向了常量表中‘3’的内存地址”

为了操作更加方便灵活,类定义中的类变量则直接被装载到类的命名空间进行管理。

将局部变量替换为局部变量表+插槽

运行时

将python字节码文件加载到虚拟机。不同的python组件会执行不同的装载行为。最终将对象加载到命名空间,进行执行。

组件加载过程

Python的加载逻辑是将文件中定义的内容 转换为 各个层级维护的命名空间中的映射关系。进行调用时 通过查询命名空间,找到对应的待执行内容(类,方法,变量),执行。

组件和类的加载:

1
2
3
4
class A:
x = 10
def __init__(self, val):
self.val = val

等价于 :

1
2
3
namespace = {}
exec(code_object_for_class_A, globals(), namespace)
A = type("A", (object,), namespace)

组件和类的加载过程:

定义一个了命名空间(内存申请)

使用code_object 加载类A中的元素定义到命名空间

再在外层将类与该命名空间绑定(实际上也将A添加到外部的命名空间)

cpython 使用code_object这个对象实现了向类中添加其所属元素的加载过程

函数/方法

方法中同样会创建一个code object 对象,但是不会将变量-引用转换成k-v模式加入到函数的命名空间,而是作为字节码存储在命名空间中,当方法被执行时,函数会调用字节码文件执行,创建一系列的参数元组,然后通过索引→key→value 的调用方式查询待处理的对象

实例的创建

在python中创建一个实例

1
2
3
4
5
6
7
8
9
10
class person :
def __init__(self,name):
self.name = name

nobody = person("nobody")
# 1. 隐式调用了__new__ 方法 person.__new__(person,name),
# 查询类命名空间中的__new__new 方法并执行,接收参数传入,是因为在实例构造期间,传入的参数并不一定只是进行字段的初始化,
# 也可能参与实例的构造过程
# 2. 调用__init__ 方法, person.__init__(self,name),
# 查询定义的__init__方法,接收传入参数并对对象中的字段进行初始化。

函数的执行

在python中调用一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def say_hello(name):
print(locals())
print("hello ", name)

if __name__ == "__main__":
print(locals())
print("variable in func say_hello "+say_hello.__code__.co_varnames[0])
say_hello("codfish")

# 输出
#{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000016CCC307850>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'd:\\Software\\age.py', '__cached__': None, 'say_hello': <function say_hello at 0x0000016CCC2DFB00>}
# 对应 外部的locals()
# variable in func say_hello name
# 对应 print("variable in func say_hello "+say_hello.__code__.co_varnames[0]) 打印say_hello
# 函数中的常量池信息0
#{'name': 'codfish'}
# 函数内部的locals()
# hello codfish
# 函数的最终打印
# 因为python 函数中的对象表是会多次创建的,所以函数中保留的是code_object 字节码,每次都
# 会执行创建对象表的过程。
# 1. 打印本地的命名空间
# 2. 查询函数中的参数信息->name
# 3. 执行say_hello,可以从命名空间中查询到say_hello并执行
# 4. 打印内部的命名空间,可以看到name与codfish 进行了绑定
# 5. 执行print打印name,内部是用索引去查询name对应的实参,但仍然维护了一个命名空间

方法的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class someone :
print(locals())
def __init__(self,name):
self.name=name
print(locals())
def say_hello(self):
print(self.__dict__)
print(locals())
print("hello ", self.name)
print(locals())

if __name__ == "__main__":
print(locals())
print("variable in func say_hello "+someone.say_hello.__code__.co_varnames[0])
print("someone's namespace:",someone.__dict__)
someone('codfish').say_hello()
# 输出
# {'__module__': '__main__', '__qualname__': 'someone', '__firstlineno__': 1}
# Python 的类定义和函数定义都是“执行过程”,不是声明语法。
# 当解释器遇到 class MyClass:,它就立即执行整个类体中的代码,从上到下顺序执行。
# def 代表执行加载这个函数定义到命名空间中
# {'__module__': '__main__', '__qualname__': 'someone', '__firstlineno__': 1, '__init__': <function someone.__init__ at 0x000001C90E681440>, 'say_hello': <function someone.say_hello at 0x000001C90E6E4720>}
# 执行过程 只会执行类声明次一级的代码,函数定义的代码块会作为字节码被存储到__code__中
# {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001C90E6F7850>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'd:\\Software\\age.py', '__cached__': None, 'someone': <class '__main__.someone'>}
# 全局命名空间
# variable in func say_hello self
# 查询到的第一个参数,对于方法第一个参数为self.
# someone's namespace: {'__module__': '__main__', '__firstlineno__': 1, '__init__': <function someone.__init__ at 0x000001C90E681440>, 'say_hello': <function someone.say_hello at 0x000001C90E6E4720>, '__static_attributes__': ('name',), '__dict__': <attribute '__dict__' of 'someone' objects>, '__weakref__': <attribute '__weakref__' of 'someone' objects>, '__doc__': None}
# 装载后的类命名空间
# {'self': <__main__.someone object at 0x000001C90E5E70E0>, 'name': 'codfish'}
# 初始化方法__init__的命名空间
# {'name': 'codfish'}
# 实例的命名空间
# {'self': <__main__.someone object at 0x000001C90E5E70E0>}
# 由函数维护的命名空间
# hello codfish
# 最终打印

命名空间的查询

1
2
3
4
locals()	 # 当前作用域(动态)	当前栈帧中的局部变量字典	查看当前函数、类体、模块的局部变量
globals() # 当前模块级作用域 当前模块的全局变量字典 查看模块的全局变量、函数、类等
__dict__ # 对象自身的属性字典 显式定义的变量(不含继承/内建属性) 查看对象自己的属性
dir() # 对象+继承链 所有可访问属性名的列表(字符串) 补全、IDE 自动提示、调试

多态

python是通过命名空间来管理每个对象的访问权限的。

所以调用都遵循着 当调用函数,类时,查询当前层的命名空间,检索对应的函数名,类名信息。找到对应的函数,类的位置,进行执行。

那么我们可以看看python是如何实现多态的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class anmial :
# 创建基类,定义eat方法
def __init__(self,name):
self.name = name
def eat(self):
print(self.name + " eat nothing, still hungry")

class duck(anmial):
def __init__(self,name="duck"):
self.name = name
# super().__init__(name)
# 定义子类继承基类,传递name 子类的self.name 和 使用super().__init__(name)对父类的self
# 进行初始化效果相同,说明self仍然是同一个实例(内存对象)。
class cat(anmial) :
# 定义子类猫,重写eat方法
def __init__(self,name="cat"):
self.name = name
def eat(self):
print(self.name + " eat some fish")

class dog(anmial):
# 定义子类狗,重新eat方法
def __init__(self,name="dog"):
self.name = name
def eat(self):
print(self.name + " eat some bones")

class ghost :
# 定义普通类鬼,具有相同的方法
def __init__(self,name="ghost") :
self.name = name
def eat(self):
print(self.name+ " eat some blood")

class person :
# 定义普通类人,不具备eat方法
def __init__(self,name="person") :
self.name = name
def want_eat(self):
print(self.name+ " eat some noodls")

def toldAnmialToEat(anmial):
# 定义了一个函数接收传入的基类,调用eat方法
print(locals())
anmial.eat()

toldAnmialToEat(duck())
print(dir(duck))
toldAnmialToEat(cat())
toldAnmialToEat(dog())
toldAnmialToEat(ghost())
toldAnmialToEat(person())
# 执行
# {'anmial': <__main__.duck object at 0x000002A92EFA6F90>}
# duck eat nothing, still hungry
# duck 未实现自己的方法所以使用了父类的方法
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', 'eat']
# duck 由父类继承了eat 方法
# {'anmial': <__main__.cat object at 0x000002A92EFA6F90>}
# cat eat some fish
# 调用了cat的方法
# {'anmial': <__main__.dog object at 0x000002A92EFA6F90>}
# dog eat some bones
# 调用了dog的方法
# {'anmial': <__main__.ghost object at 0x000002A92EFA6F90>}
# ghost eat some blood
# 调用了ghost的方法
# {'anmial': <__main__.person object at 0x000002A92EFA6F90>}
# Traceback (most recent call last):
# person 不具备此方法,报错

可以看出

1.子类会从父类那里继承它没有重新的方法。

2.当子类重写了方法后,会执行子类的方法

  1. 非子类但具有相同方法的类也可以正确执行,说明函数的定义只是声明式约束,不会进行类的检查

  2. 当传入的类不具备方法时,会报错。由3,4说明还是根据命名空间中的功能列表来进行的函数执行

变量,引用,类型和对象

通过分析,我们可以看到Python中的所有对象都是由命名空间来组织和维护的。

变量 : python中的变量名在字节码中是以名称索引的形式出现,并存于名称表中。

引用 : 引用在python中表现为一种“间接”的引用,它不是通过标识符→实际对象的地址指针→获取实际的对象。而是通过创建了标识符对象→标识符加入name表→实际对象加入常量表→通过命名空间完成映射绑定。再通过命名空间中的k-v关系查询到实际被调用的对象。

类型 : Python是弱类型语言的正是基于这种引用的实现来构建的。虽然在底层Python的对象依然是存在类型的,但键与值之间的关系是基于映射关系,而不是指针。由于键值映射的动态性,类型检查在运行时进行,名称本身不携带类型信息

对象 :同样是因为这种引用的构建,Python中的一切皆对象实施的更加彻底——- 变量名也是作为对象存储的。这样的设计让我们不必深入底层才能获取到类,函数的创建信息,只需要找到其中的映射关系,就能对类或者函数中的属性进行操作修改。


Python的加载过程与内存模型
http://gadoid.io/2025/04/14/Python的加载过程与内存模型/
作者
Codfish
发布于
2025年4月14日
许可协议