一 基本环境
1.1 虚拟机
当虚拟机开始运行时, 通过初始化函数完成整个运行环境的设置
• 创建解释器和主线程状态对象,这是整个进程的根对象。
• 初始化内置类型。数字、列表等类型都有专⻔门的缓存策略需要处理。
• 创建 __builtin__ 模块,该模块持有所有内置类型和函数。
• 创建 sys 模块,其中包含了 sys.path、modules 等重要的运行行期信息。
• 初始化 import 机制。
• 初始化内置 Exception。
• 创建 __main__ 模块,准备运行行所需的名字空间。
• 通过 site.py 将 site-packages 中的第三方方扩展库添加到搜索路径列表。
• 执行行入入口口 py 文文件。执行行前会将 __main__.__dict__ 作为名字空间传递进去。
• 程序执行行结束。
• 执行行清理操作,包括调用用退出函数,GC 清理现场,释放所有模块等。
• 终止止进程。
1.2 类型和对象
先有类型 (Type),而而后才能生生成实例 (Instance)。Python 中的一一切都是对象,包括类型在内的每
个对象都包含一一个标准头,通过头部信息就可以明确知道其具体类型。
头信息由"引用计数" 和 "类型指针"组成,以int为例:
#define PyObject_HEAD
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
typedef struct _object {
PyObject_HEAD
} PyObject;
typedef struct {
PyObject_HEAD // 在 64 位版本中,头⻓长度为 16 字节。
long ob_ival; // long 是 8 字节。
} PyIntObject;
使用sys中的函数测试
>>> import sys
>>> x = 0x1234 # 不能使用[-5, 257], 他们用专门的缓存机制
>>> sys.getsizeof(x) # 符合预期长度
28
>>> sys.getrefcount(x) # sys.getrefcount() 读取头部引用用计数, 这里行参也会增加一次引用计数, 所以是2
2
>>> y = x
>>> sys.getrefcount(x)
3
>>> del y
>>> sys.getrefcount(x)
2
类型指针则指向具体的类型对象,其中包含了继承关系、静态成员等信息。所有的内置类型对象都
能从 types 模块中找到,至至于 int、long、str 这些关键字可以看做是简短别名。
>>> x.__class__ # __class__通过类型指针来获取类型对象
<class 'int'>
>>> x.__class__ is type(x)
True
>>> type(x) is int # is 通过指针判断是否指向同一对象
True
>>> x.__class__ is int is type(x)
True
1.3 名字空间
和C变量名是内存地址别名不同, python的名字(变量)实际上是一个字符串对象,它和所指向的目标对象一起在名字空间构成一项{name:object}关联。
python有多种名字空间,不同的名字空间决定了对象的作用域和生存周期
- globas 模块名字空间
- locals 函数堆栈帧名字空间
- class, instance 名字空间
>>> x = 123
>>> globals()
{...'x': 123, '__builtins__': <module 'builtins' (built-in)>...}
名字空间就是一个字典, 可以在名字空间直接添加项来创建名字(变量)
>>> globals()['y'] = 'hello world'
>>> y
'hello world'
名字的作用用仅仅是在某个时刻与名字空间中的某个对象进行行关联。其本身身不包含⺫目目标对象的任何信息,只有通过对象头部的类型指针才能获知其具体类型,进而而查找其相关成员数据。正因为名字的
弱类型特征,我们可以在运行行期随时将其关联到任何类型对象
>>> y
'hello world'
>>> type(y)
<type 'str'>
>>> y = __import__('string') # 将原本与字符串关联的名字指向模块对象
>>> type(y)
<type 'module'>
>>> y.digits # 查看模块对象的成员
'0123456789'
在函数外部, locals() 和 globals() 作用用完全相同。而而当在函数内部调用用时,locals() 则是获取当前
函数堆栈帧的名字空间,其中存储的是函数参数、局部变量等信息。
>>> globals() is locals()
True
>>> locals()
{'y': <module 'string' from '/usr/lib/python2.7/string.pyc'>, '__name__': '__main__'}
>>> def test(x):
... y = x + 100
... print(locals()) # 可以看到 locals 名字空间中包含当前局部变量 {'x': 123, 'y': 223}
... print(globals() is locals()) # 此时 locals 和 globals 指向不同名字空间 False
...
... frame = sys._getframe(0) # _getframe(0) 获取当前堆栈帧
... print(locals() is frame.f_locals) # locals 名字空间实际就是当前堆栈帧的名字空间
... print(globals() is frame.f_globals) # 通过 frame 我们也可以函数定义模块的名字空间
...
>>> test(123)
{'x': 123, 'y': 223}
False
True
True
1.4 内存管理
为提升执行行性能,Python 在内存管理上做了大量工作。最直接的做法就是用用内存池来减少操作系
统内存分配和回收操作,那些小于等于 256 字节对象,将直接从内存池中获取存储空间。
根据需要,虚拟机每次从操作系统申请一一块 256KB,取名为 arena 的大块内存。并按系统页大
小,划分成多个 pool。每个 pool 继续分割成 n 个大小相同的 block,这是内存池最小存储单位。
14block 大小是 8 的倍数,也就是说存储 13 字节大小的对象,需要找 block 大小为 16 的 pool 获
取空闲块。所有这些都用用头信息和链表管理起来,以便快速查找空闲区域进行行分配。
大于 256 字节的对象,直接用用 malloc 在堆上分配内存。程序运行行中的绝大大多数对象都小于这个阈
值,因此内存池策略可有效提升性能。
当所有 arena 的总容量超出限制 (64MB) 时,就不再请求新的 arena 内存。而而是如同 "大对象" 一一
样,直接在堆上为对象分配内存。另外,完全空闲的 arena 会被释放,其内存交还给操作系统
引用传递
对象总是按引用传递,简单点说就是通过复制指针来实现多个名字指向同一对象。因为 arena 也是
在堆上分配的,所以无无论何种类型何种大大小小的对象,都存储在堆上
>>> a = object()
>>> b = a
>>> a is b
True
>>> hex(id(a)), hex(id(b))
# 地址相同,意味着对象是同一一个。
('0x10b1f5640', '0x10b1f5640')
>>> def test(x):
...
print hex(id(x))
>>> test(a)
0x10b1f5640
如果不希望对象被修改,就需使用不可变类型,或对象复制品
不可变类型:int, long, str, tuple, frozenset
除了某些类型自自带的 copy 方方法外,还可以:
• 使用用标准库的 copy 模块进行行深度复制。
• 序列化对象,如 pickle、cPickle、marshal
下面的测试建议不要用数字等不可变对象,因为其内部的缓存和复用用机制可能会造成干扰。
>>> import copy
>>> x = object()
>>> l = [x]
>>> l2 = copy.copy(l) # 浅复制,仅复制列表对象,而不会递归复制列表成员
>>> l2 is l
False
>>> l2[0] is l[0]
True
>>> l2[0] is l[0] is x # 复制列表的元素依然是原对象
True
>>> l3 = copy.deepcopy(l) # 深度赋值,递归复制所有成员
>>> l3 is l
False
>>> l3[0] is l[0] # 列表元素也被复制
False
>>> l3[0] is x
False
循环引用会影响deepcopy函数的运作, 建议查看官方标准库文档
引用计数
python默认采用引用计数来管理对象的内存回收, 当引用计数为0时,将立即回收该对象内存,要么将对应的block块标记为空闲,要么返还操作系统。
>>> class User(object):
... def __del__(self):
... print('dead')
...
>>> a = User()
>>> b = a
>>> del a
>>> del b
dead
某些内置内型,比如小整数,因为缓存的缘故,计数永远不会为0, 直到进程结束才由虚拟机清理函数释放。
Python 还支支弱引用。允许在不增加引用计数,不妨碍对象回收的情况下间接引用对象。但不是所有类型都支持弱引用,比比如 list、dict ,弱引用会引发异常。
>>> class User(object):pass
...
>>> def callback(r):
... print('weakref object:',r)
... print('object dead')
...
>>> a = User()
>>> import weakref
>>> r = weakref.ref(a, callback) # 创建弱引用用对象。
>>> sys.getrefcount(a) # 计数2是getrefcount(a)行参造成的,可以看到目标对象引用计算没有增加
2
>>> r() is a # 弱引用可以访问对象
True
>>> del a # 原对象回收,callback 被调用用
weakref object: <weakref at 0x7f9ef6202d68; dead>
object dead
>>> hex(id(r)) # 通过对比比,可以看到 callback 参数是弱引用用对象
'0x7f9ef6202d68' # 因为原对象已经死亡
>>> r() is None # 此时弱引用用只能返回 None。也可以此判断原对象死亡
True
简单明显的循环引用,可以用弱引用打破循环关系。但在实际开发中,循环
引用的形成往往很复杂,可能由 n 个对象间接形成一个大的循环体,此时只有靠 GC 去回收了。
>>> class Data(object):
... def __init__(self, owner):
... self.owner = weakref.ref(owner)
... print(self.owner)
... def __del__(self):
... print('in Data del')
...
>>> class Node(object):
... def __init__(self):
... self.data = Data(self)
... def __del__(self):
... print('in Node del')
...
>>> node = Node()
<weakref at 0x7f9ef6202e08; to 'Node' at 0x7f9ef6207d30>
>>> del node
in Node del
in Data del
垃圾回收
Python 拥有两套垃圾回收机制。除了引用计数,还有个专门处理循环引用的 GC。
能引发循环引用问题的,都是那种容器类对象,比如 list、set、object 等。对于这类对象,虚拟
机在为其分配内存时,会额外添加用于追踪的 PyGC_Head。这些对象被添加到特殊链表里,以便
GC 进行管理。
typedef union _gc_head {
struct {
union _gc_head *gc_next;
union _gc_head *gc_prev;
Py_ssize_t gc_refs;
} gc;
long double dummy;
} PyGC_Head;
如果不存在循环引用,自然是积极性更高的引用计数机制抢先给处理掉。也就是说,只要不存在循环引用,理论上可以禁用 GC。当执行某些密集运算时,临时关掉 GC 有助于提升性能。
>>> import gc
>>> class User(object):
... def __del__(self):
... print(hex(id(self)), 'will be dead')
...
>>> gc.disable()
>>> a = User()
>>> del a # 对象正常回收,引用计数不依赖gc
0x7f9ef6207d30 will be dead
同 .NET、JAVA 一一样,Python GC 同样将要回收的对象分成 3 级代龄。GEN0 管理新近加入的年
轻对象,GEN1 则是在上次回收后依然存活的对象,剩下 GEN2 存储的都是生命周期极长的家伙。
每级代龄都有一一个最大容量阈值,每次 GEN0 对象数量超出阈值时,都将引发垃圾回收操作
#define NUM_GENERATIONS 3
/* linked lists of container objects */
static struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
{{{GEN_HEAD(0), GEN_HEAD(0), 0}}, 700, 0},
{{{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0},
{{{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0},
};
GC 首先检查 GEN2,如阈值被突破,那么合并 GEN2、GEN1、GEN0 几个追踪链表。如果没有超
出,则检查 GEN1。GC 将存活的对象提升代龄,而而那些可回收对象则被打破循环用用,放到专⻔
的列表等待回收。
>>> gc.get_threshold() # 获取各级代龄阈值
(700, 10, 10)
>>> gc.get_count() # 各级代龄链表跟踪的对象数量
(198, 8, 3)
包含 __del__ 方方法的循环引用用对象,永远不会被 GC 回收,直至至进程终止(这种情况python3是可以回收的)。
这回不能偷懒用 __del__ 监控对象回收了,改用 weakref。因 IPython 对 GC 存在干扰,下面的测
试代码建议在原生 shell 中进行
>>> import gc, weakref
>>> class User(object): pass
...
>>> def callback(r): print(r, 'will be dead')
...
>>> gc.disable()
>>>
>>> a = User();wa = weakref.ref(a, callback)
>>> b = User();wb = weakref.ref(b, callback)
>>> a.b = b; b.a = a # 形成循环引用
>>> a.b
<__main__.User object at 0x7f70558edcc0>
>>> del a; del b # 删除名字引用用。
>>> wa(), wb() # 显然,计数机制对循环引用用无无效。
(<__main__.User object at 0x7f70558f7390>, <__main__.User object at 0x7f70558edcc0>)
>>> gc.enable() # 开启gc
>>> gc.isenabled()
True
>>>
>>> gc.collect() # 因为没有达到阈值,我们手手工工启动回收。
<weakref at 0x7f70558eb5e8; dead> will be dead
<weakref at 0x7f70558eb778; dead> will be dead # 这个地址是弱引用用对象的,别犯糊涂。
4
>>>
一旦有了 __del__,GC 就拿循环引用没办法了。GC也无法回收(python2, 未测试)。
python3经过测试是可以回收的
import gc, weakref
class User(object):
def __del__(self):
print(hex(id(self)), 'be del in User')
def callback(r): print(r, 'will be dead in callback')
a = User();wa = weakref.ref(a, callback)
b = User();wb = weakref.ref(b, callback)
a.b = b; b.a = a
gc.isenabled()
gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK) # 设置输出更详细的回收状态信息
del a; del b
gc.collect()
gc: collecting generation 2...
gc: objects in each generation: 137 2996 12259
gc: collectable <User 0x7ff3466da320>
gc: collectable <User 0x7ff3466fad68>
gc: collectable <dict 0x7ff3466d2e48>
gc: collectable <dict 0x7ff347e2acc8>
<weakref at 0x7ff3466ce778; dead> will be dead in callback
<weakref at 0x7ff34600b818; dead> will be dead in callback
0x7ff3466da320 be del in User
0x7ff3466fad68 be del in User
gc: done, 4 unreachable, 0 uncollectable, 0.0084s elapsed
4
1.5 编译
Python 实现了栈式虚拟机 (Stack-Based VM) 架构,通过与机器无关的字节码来实现跨平台执行
能力。这种字节码指令集没有寄存器,完全以栈 (抽象层面面) 进行指令运算。尽管很简单,但对普通
开发人员而言,是无需关心的细节。
要运行 Python 语言编写的程序,必须将源码编译成字节码。通常情况下,编译器会将源码转换成
字节码后保存在 pyc 文件中。还可用用 -O 参数生成 pyo 格式,这是简单优化后的 pyc 文件。
编译发生在模块载入那一刻。具体来看,又分为 pyc 和 py 两种情况。
载入 pyc 流程:
• 核对文件 Magic 标记。
• 检查时间戳和源码文件修改时间是否相同,以确定是否需要重新编译。
• 载入模块。
如果没有 pyc,那么就需要先完成编译:
• 对源码进行 AST 分析。
• 将分析结果编译成 PyCodeObject。
• 将 Magic、源码文件修改时间、PyCodeObject 保存到 pyc 文件中。
• 载入模块。
如果对 pyc 文件格式有兴趣,但又不想看 C 代码,可以到 /usr/lib/python2.7/compiler 目录里
寻宝。又或者你对反汇编、代码混淆、代码注入等话题更有兴趣,不妨看看标准库里的 dis。
1.6 执行
最简单的就是用用eval() 执行行表达式。
eval 默认会使用当前环境的名字空间,当然我们也可以带入自定义字典
>>> eval('(1+2)*3') # 使用用当前上下文文的名字空间
9
>>> x = 10
>>> eval('x+200')
210
>>> ns = dict(x =10, y =20)
>>> eval('x+y',ns) # 使用自定义空间
30
>>> ns.keys()
dict_keys(['y', 'x', '__builtins__']) # 名字空间里里多了 __builtins__。
要执行代码片段,或者 PyCodeObject 对象,那么就需要动用 exec 。同样可以带入自定义名字空
间,以避免对当前环境造成污染
总结
虚拟机
虚拟机运行时执行的一些初始化操作
创建解释器和主线程状态对象,这是整个进程的根对象。
初始化内置类型。数字、列表等类型都有专⻔门的缓存策略需要处理。
类型和对象
先有类型 (Type),而而后才能生生成实例 (Instance),头部有引用计数和类型指针组成
名字空间
- globas 模块名字空间 globas()是一个字典, 可以往里面添加名字
- locals 函数堆栈帧名字空间 在函数外 globas() is locals() ,在函数类存储的函数参数局部变量
- class, instance 名字空间
内存管理
引用计数 用 __del__ 监控对象释放 。无法回收循环引用的对象
gc管理 在python3中经过测试发现是可以回收 含义__del__的循环引用对象,python2不可以