1.1 虚拟机
Python 是⼀种半编译半解释型运⾏环境。⾸先,它会在模块 载⼊时将源码编译成字节码 (ByteCode)。⽽后,这些字节码会被虚拟机在⼀个 巨⼤的核⼼函数⾥解释执⾏。这是导致 Python 能较低的重要原因,好在现在有了内置 Just-in-time ⼆次编译器的 PyPy 可供选择。当虚拟机开始运⾏时,它通过初始化函数完成整个运⾏环境设置:
创建解释器和主线程状态对象,这是整个进程的根对象。
初始化内置类型。数字、列表等类型都有专⻔的缓存策略需要处理。
创建 __builtin__
模块,该模块持有所有内置类型和函数。
创建 sys 模块,其中包含了 sys.pathmodules
等重要的运⾏期信息。
初始化 import
机制。
初始化内置 Exception

创建 __main__
模块,准备运⾏所需的名字空间。
通过 site.py site-packages
中的第三⽅扩展库添加到搜索路径列表。
执⾏⼊⼝ py ⽂件。执⾏前会将 __main__.__dict__
作为名字空间传递进去。
程序执⾏结束。
执⾏清理操作,包括调⽤退出函数, GC
清理现场,释放所有模块等。
终⽌进程。
Python
源码是个宝库,其中有⼤量的编程范式和技巧可供借鉴,尤其是对内存的管理分配。个⼈建议有 C 基础的兄弟,在闲暇时翻看⼀⼆。
1.2 类型和对象
先有类型 (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 模块中找到,⾄于 intlongstr 这些关键字可以看做是简短别名。

>>> 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 这样的固定长度类型外,还有 longstr 这类变⻓对象。其头部多出⼀个记录元素项数量的字段。⽐如 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;

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


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

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

我们习惯于将 x 称为变量,但在这⾥,更准确的词语是 名字C 变量名是内存地址别名不同, Python 的名字实际上是⼀个字符串对象,它和所指向的目标对象⼀起在名字空间中构成⼀项 {name: object} 关联。Python 有多种名字空间,⽐如称为 globals 的模块名字空间,称为 locals 的函数堆栈帧名字空间,还有 classinstance 名字空间。不同的名字空间决定了对象的作⽤域和⽣存周期

>>> 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>,
}

可通过 <module>.__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 查找规则、成员查找规则等等。所有这些,都将
在相关章节中给出详细说明。
使⽤名字空间管理上下⽂对象,带来⽆与伦⽐的灵活性,但也牺牲了执⾏性能。毕竟从字典中查找
对象远⽐指针低效很多,各有得失。


1.4 内存管理
为提升执⾏性能, 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
模块进⾏深度复制。
序列化对象,如 picklecPicklemarshal

下⾯的测试建议不要⽤数字等不可变对象,因为其内部的缓存和复⽤机制可能会造成干扰。

>>> 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 还⽀持弱引⽤。允许在不增加引⽤计数,不妨碍对象回收的情况下间接引⽤对象。但不是所有类型都⽀持弱引⽤,⽐如 listdict ,弱引⽤会引发异常。

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

>>> 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”能引发循环引⽤问题的,都是那种容器类对象,⽐如 listsetobject 等。对于这类对象,虚拟机在为其分配内存时,会额外添加⽤于追踪的 PyGC_Head。这些对象被添加到特殊链表⾥,以便GC 进⾏管理。

typedef union _gc_head {
struct {
union _gc_head *gc_next;
17union _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!

.NETJAVA ⼀样, 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,如阈值被突破,那么合并 GEN2GEN1GEN0 ⼏个追踪链表。如果没有超出,则检查 GEN1GC 将存活的对象提升代龄,⽽那些可回收对象则被打破循环引⽤,放到专门的列表等待回收。
>>> 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...
19gc: 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.
>>> 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__ 承担了析构函数的⾓⾊,某些时候还是有其特定的作⽤的。⽤弱引⽤回调会造成逻辑分离,不便于维护。对于⼀些简单的脚本,我们还是能保证避免循环引⽤的,那不妨试试。就像前⾯例⼦中⽤来监测对象回收,就很⽅便。
1.5 编译
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')
2

除了内置 compile 函数,标准库⾥还有 py_compilecompileall 可供选择。

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

1.6 执⾏
相⽐ .NETJAVA CodeDOM EmitPython 天⽣拥有⽆与伦⽐的动态执⾏优势。最简单的就是⽤ 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 模块。

打赏