离线下载
PDF版 ePub版

qyuhen · 更新于 2018-11-28 11:00:43

基本环境

虚拟机

Python 是一种半编译半解释型运行环境。首先,它会在模块 "载入" 时将源码编译成字节码 (Byte Code)。而后,这些字节码会被虚拟机在一个 "巨大" 的核心函数里解释执行。这是导致 Python 性能较低的重要原因,好在现在有了内置 Just-in-time 二次编译器的 PyPy 可供选择。

当虚拟机开始运行时,它通过初始化函数完成整个运行环境设置:

  • 创建解释器和主线程状态对象,这是整个进程的根对象。
  • 初始化内置类型。数字、列表等类型都有专门的缓存策略需要处理。
  • 创建 builtin 模块,该模块持有所有内置类型和函数。
  • 创建 sys 模块,其中包含了 sys.path、modules 等重要的运行期信息。
  • 初始化 import 机制。
  • 初始化内置 Exception。
  • 创建 main 模块,准备运行所需的名字空间。
  • 通过 site.py 将 site-packages 中的第三方扩展库添加到搜索路径列表。
  • 执行入口 py 文件。执行前会将 main.dict 作为名字空间传递进去。
  • 程序执行结束。
  • 执行清理操作,包括调用退出函数,GC 清理现场,释放所有模块等。
  • 终止进程。

Python 源码是个宝库,其中有大量的编程范式和技巧可供借鉴,尤其是对内存的管理分配。个人建议有 C 基础的兄弟,在闲暇时翻看一二。

类型和对象

先有类型 (Type),而后才能生成实例 (Instance)。Python 中的一切都是对象,包括类型在内的每个对象都包含一个标准头,通过头部信息就可以明确知道其具体类型。

头信息由 "引用计数" 和 "类型指针" 组成,前者在对象被引用时增加,超出作用域或手工释放后减小,等于 0 时会被虚拟机回收 (某些被缓存的对象计数器永远不会为 0)。

以 int 为例,对应 Python 结构定义是:

#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) # 符合长度预期。
24

>>> sys.getrefcount(x) # sys.getrefcount() 读取头部引用计数,注意形参也会增加一次引用。
2

>>> y = x   # 引用计数增加。
>>> sys.getrefcount(x)
3

>>> del y   # 引用计数减小。
>>> sys.getrefcount(x)
2

类型指针则指向具体的类型对象,其中包含了继承关系、静态成员等信息。所有的内置类型对象都能从 types 模块中找到,至于 int、long、str 这些关键字可以看做是简短别名。

>>> import types

>>> x = 20

>>> type(x) is types.IntType  # is 通过指针判断是否指向同一对象。
True

>>> x.__class__    # __class__ 通过类型指针来获取类型对象。
<type 'int'>

>>> x.__class__ is type(x) is int is types.IntType
True

>>> y = x

>>> hex(id(x)), hex(id(y))  # id() 返回对象标识,其实就是内存地址。
('0x7fc5204103c0', '0x7fc5204103c0')
>>> hex(id(int)), hex(id(types.IntType))
('0x1088cebd8', '0x1088cebd8')

除了 int 这样的固定长度类型外,还有 long、str 这类变长对象。其头部多出一个记录元素项数量的字段。比如 str 的字节数量,list 列表的长度等等。

#define PyObject_VAR_HEAD \
    PyObject_HEAD \
    Py_ssize_t ob_size;  /* Number of items in variable part */

typedef struct {
    PyObject_VAR_HEAD
} PyVarObject;

有关类型和对象更多的信息,将在后续章节中详述。

名字空间

名字空间是 Python 最核心的内容。

>>> x
NameError: name 'x' is not defined

我们习惯于将 x 称为变量,但在这里,更准确的词语是 "名字"。

和 C 变量名是内存地址别名不同,Python 的名字实际上是一个字符串对象,它和所指向的目标对象一起在名字空间中构成一项 {name: object} 关联。

Python 有多种名字空间,比如称为 globals 的模块名字空间,称为 locals 的函数堆栈帧名字空间,还有 class、instance 名字空间。不同的名字空间决定了对象的作用域和生存周期。

>>> x = 123

>>> globals()   # 获取 module 名字空间。
{'x': 123, ......}

可以看出,名字空间就是一个字典 (dict)。我们完全可以直接在名字空间添加项来创建名字。

>>> globals()["y"] = "Hello, World"

>>> y
'Hello, World'

在 Python 源码中,有这样一句话:Names have no type, but objects do.

名字的作用仅仅是在某个时刻与名字空间中的某个对象进行关联。其本身不包含目标对象的任何信息,只有通过对象头部的类型指针才能获知其具体类型,进而查找其相关成员数据。正因为名字的弱类型特征,我们可以在运行期随时将其关联到任何类型对象。

>>> y
'Hello, World'

>>> type(y)
<type 'str'>

>>> y = __import__("string") # 将原本与字符串关联的名字指向模块对象。

>>> type(y)
<type 'module'>

>>> y.digits   # 查看模块对象的成员。
'0123456789'

在函数外部,locals() 和 globals() 作用完全相同。而当在函数内部调用时,locals() 则是获取当前函数堆栈帧的名字空间,其中存储的是函数参数、局部变量等信息。

>>> import sys

>>> globals() is locals()
True

>>> locals()
{
    '__builtins__': <module '__builtin__' (built-in)>,
    '__name__': '__main__',
    'sys': <module 'sys' (built-in)>,
}

>>> def test(x):     # 请对比下面的输出内容。
... y = x + 100
... print locals()    # 可以看到 locals 名字空间中包含当前局部变量。
... print globals() is locals()  # 此时 locals 和 globals 指向不同名字空间。

... frame = sys._getframe(0)   # _getframe(0) 获取当前堆栈帧。
... print locals() is frame.f_locals # locals 名字空间实际就是当前堆栈帧的名字空间。
... print globals() is frame.f_globals # 通过 frame 我们也可以函数定义模块的名字空间。

>>> test(123)
{'y': 223, 'x': 123}
False
True
True

在函数中调用 globals() 时,总是获取包含该函数定义的模块名字空间,而非调用处。

>>> pycat test.py

a = 1
def test():
    print {k:v for k, v in globals().items() if k = "__builtins__"}

>>> import test

>>> test.test()
{
    '__file__': 'test.pyc',
    '__name__': 'test',
    'a': 1,
     'test': <function test at 0x10bd85e60>,
}

可通过 .dict 访问其他模块的名字空间。

>>> test.__dict__      # test 模块的名字空间
{
    '__file__': 'test.pyc',
    '__name__': 'test',
    'a': 1,
    'test': <function test at 0x10bd85e60>,
}

>>> import sys

>>> sys.modules[__name__].__dict__ is globals() # 当前模块名字空间和 globals 相同。
True

与名字空间有关的内容很多,比如作用域、LEGB 查找规则、成员查找规则等等。所有这些,都将在相关章节中给出详细说明。

使用名字空间管理上下文对象,带来无与伦比的灵活性,但也牺牲了执行性能。毕竟从字典中查找对象远比指针低效很多,各有得失。

内存管理

为提升执行性能,Python 在内存管理上做了大量工作。最直接的做法就是用内存池来减少操作系统内存分配和回收操作,那些小于等于 256 字节对象,将直接从内存池中获取存储空间。

根据需要,虚拟机每次从操作系统申请一块 256KB,取名为 arena 的大块内存。并按系统页大小,划分成多个 pool。每个 pool 继续分割成 n 个大小相同的 block,这是内存池最小存储单位。

block 大小是 8 的倍数,也就是说存储 13 字节大小的对象,需要找 block 大小为 16 的 pool 获取空闲块。所有这些都用头信息和链表管理起来,以便快速查找空闲区域进行分配。

大于 256 字节的对象,直接用 malloc 在堆上分配内存。程序运行中的绝大多数对象都小于这个阈值,因此内存池策略可有效提升性能。

当所有 arena 的总容量超出限制 (64MB) 时,就不再请求新的 arena 内存。而是如同 "大对象" 一样,直接在堆上为对象分配内存。另外,完全空闲的 arena 会被释放,其内存交还给操作系统。

引用传递

对象总是按引用传递,简单点说就是通过复制指针来实现多个名字指向同一对象。因为 arena 也是在堆上分配的,所以无论何种类型何种大小的对象,都存储在堆上。Python 没有值类型和引用类型一说,就算是最简单的整数也是拥有标准头的完整对象。

>>> 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 x
True

>>> l3 = copy.deepcopy(l) # 深度复制,会递归复制所有深度成员。
>>> l3 is l    # 列表元素也被复制了。
False
>>> l3[0] is x
False

循环引用会影响 deepcopy 函数的运作,建议查阅官方标准库文档。

引用计数

Python 默认采用引用计数来管理对象的内存回收。当引用计数为 0 时,将立即回收该对象内存,要么将对应的 block 块标记为空闲,要么返还给操作系统。

为观察回收行为,我们用 del 监控对象释放。

>>> class User(object):
...     def __del__(self):
...         print "Will be dead"

>>> a = User()
>>> b = a

>>> import sys
>>> sys.getrefcount(a)
3

>>> del a    # 删除引用,计数减小。
>>> sys.getrefcount(b)
2

>>> del b    # 删除最后一个引用,计数器为 0,对象被回收。
Will be dead

某些内置类型,比如小整数,因为缓存的缘故,计数永远不会为 0,直到进程结束才由虚拟机清理函数释放。

除了直接引用外,Python 还支持弱引用。允许在不增加引用计数,不妨碍对象回收的情况下间接引用对象。但不是所有类型都支持弱引用,比如 list、dict ,弱引用会引发异常。

改用弱引用回调监控对象回收。

>>> import sys, weakref

>>> class User(object): pass

>>> def callback(r):   # 回调函数会在原对象被回收时调用。
...      print "weakref object:", r
...      print "target object dead"

>>> a = User()

>>> r = weakref.ref(a, callback) # 创建弱引用对象。

>>> sys.getrefcount(a)   # 可以看到弱引用没有导致目标对象引用计数增加。
2      # 计数 2 是因为 getrefcount 形参造成的。

>>> r() is a    # 透过弱引用可以访问原对象。
True

>>> del a     # 原对象回收,callback 被调用。
weakref object: <weakref at 0x10f99a368; dead>
target object dead

>>> hex(id(r))    # 通过对比,可以看到 callback 参数是弱引用对象。
'0x10f99a368'    # 因为原对象已经死亡。

>>> r() is None    # 此时弱引用只能返回 None。也可以此判断原对象死亡。
True

引用计数是一种简单直接,并且十分高效的内存回收方式。大多数时候它都能很好地工作,除了循环引用造成计数故障。简单明显的循环引用,可以用弱引用打破循环关系。但在实际开发中,循环引用的形成往往很复杂,可能由 n 个对象间接形成一个大的循环体,此时只有靠 GC 去回收了。

垃圾回收

事实上,Python 拥有两套垃圾回收机制。除了引用计数,还有个专门处理循环引用的 GC。通常我们提到垃圾回收时,都是指这个 "Reference Cycle Garbage Collection"。

能引发循环引用问题的,都是那种容器类对象,比如 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。当执行某些密集运算时,临时关掉 GC 有助于提升性能。

>>> import gc

>>> class User(object):
...     def __del__(self):
...         print hex(id(self)), "will be dead"

>>> gc.disable()    # 关掉 GC

>>> a = User()  
>>> del a     # 对象正常回收,引用计数不会依赖 GC。
0x10fddf590 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()   # 各级代龄链表跟踪的对象数量
(203, 0, 5)

包含 del 方法的循环引用对象,永远不会被 GC 回收,直至进程终止。

这回不能偷懒用 del 监控对象回收了,改用 weakref。因 IPython 对 GC 存在干扰,下面的测试代码建议在原生 shell 中进行。

>>> import gc, weakref

>>> class User(object): pass
>>> def callback(r): print r, "dead"

>>> gc.disable()     # 停掉 GC,看看引用计数的能力。

>>> a = User(); wa = weakref.ref(a, callback)
>>> b = User(); wb = weakref.ref(b, callback)

>>> a.b = b; b.a = a    # 形成循环引用关系。

>>> del a; del b     # 删除名字引用。
>>> wa(), wb()     # 显然,计数机制对循环引用无效。
(<__main__.User object at 0x1045f4f50>, <__main__.User object at 0x1045f4f90>)

>>> gc.enable()     # 开启 GC。
>>> gc.isenabled()     # 可以用 isenabled 确认。
True

>>> gc.collect()     # 因为没有达到阈值,我们手工启动回收。
<weakref at 0x1045a8cb0; dead> dead  # GC 的确有对付基友的能力。 
<weakref at 0x1045a8db8; dead> dead  # 这个地址是弱引用对象的,别犯糊涂。

一旦有了 del,GC 就拿循环引用没办法了。

>>> import gc, weakref

>>> class User(object):
... def __del__(self): pass    # 难道连空的 __del__ 也不行?

>>> def callback(r): print r, "dead"

>>> gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK) # 输出更详细的回收状态信息。
>>> gc.isenabled()      # 确保 GC 在工作。
True

>>> a = User(); wa = weakref.ref(a, callback)
>>> b = User(); wb = weakref.ref(b, callback)
>>> a.b = b; b.a = a

>>> del a; del b
>>> gc.collect()      # 从输出信息看,回收失败。
gc: collecting generation 2...
gc: objects in each generation: 520 3190 0
gc: uncollectable <User 0x10fd51fd0>   # a
gc: uncollectable <User 0x10fd57050>   # b
gc: uncollectable <dict 0x7f990ac88280>  # a.__dict__
gc: uncollectable <dict 0x7f990ac88940>  # b.__dict__
gc: done, 4 unreachable, 4 uncollectable, 0.0014s elapsed.
4

>>> xa = wa()
>>> xa, hex(id(xa.__dict__))
<__main__.User object at 0x10fd51fd0>, '0x7f990ac88280',

>>> xb = wb()
>>> xb, hex(id(xb.__dict__))
<__main__.User object at 0x10fd57050>, '0x7f990ac88940'

关于用不用 del 的争论很多。大多数人的结论是坚决抵制,诸多 "牛人" 也是这样教导新手的。可毕竟 del 承担了析构函数的角色,某些时候还是有其特定的作用的。用弱引用回调会造成逻辑分离,不便于维护。对于一些简单的脚本,我们还是能保证避免循环引用的,那不妨试试。就像前面例子中用来监测对象回收,就很方便。

编译

Python 实现了栈式虚拟机 (Stack-Based VM) 架构,通过与机器无关的字节码来实现跨平台执行能力。这种字节码指令集没有寄存器,完全以栈 (抽象层面) 进行指令运算。尽管很简单,但对普通开发人员而言,是无需关心的细节。

要运行 Python 语言编写的程序,必须将源码编译成字节码。通常情况下,编译器会将源码转换成字节码后保存在 pyc 文件中。还可用 -O 参数生成 pyo 格式,这是简单优化后的 pyc 文件。

编译发生在模块载入那一刻。具体来看,又分为 pyc 和 py 两种情况。

载入 pyc 流程:

  • 核对文件 Magic 标记。
  • 检查时间戳和源码文件修改时间是否相同,以确定是否需要重新编译。
  • 载入模块。

如果没有 pyc,那么就需要先完成编译:

  • 对源码进行 AST 分析。
  • 将分析结果编译成 PyCodeObject。
  • 将 Magic、源码文件修改时间、PyCodeObject 保存到 pyc 文件中。
  • 载入模块。

Magic 是一个特殊的数字,由 Python 版本号计算得来,作为 pyc 文件和 Python 版本检查标记。PyCodeObject 则包含了代码对象的完整信息。

typedef struct {
    PyObject_HEAD
    int co_argcount;  // 参数个数,不包括 *args, **kwargs。
    int co_nlocals;  // 局部变量数量。
    int co_stacksize;  // 执行所需的栈空间。
    int co_flags;   // 编译标志,在创建 Frame 时用得着。
    PyObject *co_code;  // 字节码指令。
    PyObject *co_consts;  // 常量列表。
    PyObject *co_names;  // 符号列表。
    PyObject *co_varnames; // 局部变量名列表。
    PyObject *co_freevars; // 闭包: 引用外部函数名字列表。
    PyObject *co_cellvars; // 闭包: 被内部函数引用的名字列表。
    PyObject *co_filename; // 源码文件名。
    PyObject *co_name;  // PyCodeObject 的名字,函数名、类名什么的。
    int co_firstlineno;  // 这个 PyCodeObject 在源码文件中的起始位置,也就是行号。
    PyObject *co_lnotab;  // 字节码指令偏移量和源码行号的对应关系,反汇编时用得着。
    void *co_zombieframe;  // 为优化准备的特殊 Frame 对象。
    PyObject *co_weakreflist; // 为弱引用准备的...
} PyCodeObject;

无论是模块还是其内部的函数,都被编译成 PyCodeObject 对象。内部成员都嵌套到 co_consts 列表中。

>>> pycat test.py
"""
    Hello, World
"""

def add(a, b):
    return a + b

c = add(10, 20)

>>> code = compile(open("test.py").read(), "test.py", "exec")

>>> code.co_filename, code.co_name, code.co_names
('test.py', '<module>', ('__doc__', 'add', 'c'))

>>> code.co_consts
('\n Hello, World\n', <code object add at 0x105b76e30, file "test.py", line 5>, 10,
20, None)

>>> add = code.co_consts[1]
>>> add.co_varnames
('a', 'b')

除了内置 compile 函数,标准库里还有 py_compile、compileall 可供选择。

>>> import py_compile, compileall

>>> py_compile.compile("test.py", "test.pyo")
>>> ls
main.py* test.py  test.pyo

>>> compileall.compile_dir(".", 0)
Listing . ...
Compiling ./main.py ...
Compiling ./test.py ...

如果对 pyc 文件格式有兴趣,但又不想看 C 代码,可以到 /usr/lib/python2.7/compiler 目录里寻宝。又或者你对反汇编、代码混淆、代码注入等话题更有兴趣,不妨看看标准库里的 dis。

执行

相比 .NET、JAVA 的 CodeDOM 和 Emit,Python 天生拥有无与伦比的动态执行优势。

最简单的就是用 eval() 执行表达式。

>>> eval("(1 + 2) * 3")  # 假装看不懂这是啥……
9

>>> eval("{'a': 1, 'b': 2}") # 将字符串转换为 dict。
{'a': 1, 'b': 2}

eval 默认会使用当前环境的名字空间,当然我们也可以带入自定义字典。

>>> x = 100
>>> eval("x + 200")  # 使用当前上下文的名字空间。
300

>>> ns = dict(x = 10, y = 20)
>>> eval("x + y", ns)  # 使用自定义名字空间。
30

>>> ns.keys()   # 名字空间里多了 __builtins__。
['y', 'x', '__builtins__']

要执行代码片段,或者 PyCodeObject 对象,那么就需要动用 exec 。同样可以带入自定义名字空间,以避免对当前环境造成污染。

>>> py = """
... class User(object):
...     def __init__(self, name):
...         self.name = name
...     def __repr__(self):
...         return "<User: {0:x}; name={1}>".format(id(self), self.name)
... """

>>> ns = dict()
>>> exec py in ns   # 执行代码片段,使用自定义的名字空间。

>>> ns.keys()   # 可以看到名字空间包含了新的类型:User。
['__builtins__', 'User']

>>> ns["User"]("Tom")  # 完全可用。貌似用来开发 ORM 会很简单。
<User: 10547f290; name=Tom>

继续看 exec 执行 PyCodeObject 的演示。

>>> py = """
... def incr(x):
...     global z
...     z += x
... """

>>> code = compile(py, "test", "exec")   # 编译成 PyCodeObject。

>>> ns = dict(z = 100)     # 自定义名字空间。
>>> exec code in ns     # exec 执行以后,名字空间多了 incr。

>>> ns.keys()      # def 的意思是创建一个函数对象。
['__builtins__', 'incr', 'z']

>>> exec "incr(x); print z" in ns, dict(x = 50) # 试着调用这个 incr,不过这次我们提供一个
150        # local 名字空间,以免污染 global。
>>> ns.keys()      # 污染没有发生。
['__builtins__', 'incr', 'z']

动态执行一个 py 文件,可以考虑用 execfile(),或者 runpy 模块。

上一篇: 关于 下一篇: 内置类型