3.5 内存管理
到现在为止,你已经看了不少Python代码的例子。我们本节的主题是变量和内存管理的细节,包括:
变量无须事先声明;
变量无须指定类型;
程序员不用关心内存管理;
变量名会被“回收”;
del语句能够直接释放资源。
3.5.1 变量定义
大多数编译型语言,变量在使用前必须先声明,其中的C语言更加苛刻:变量声明必须位于代码块最开始,且在任何其他语句之前。其他语言,像C++和Java,允许“随时随地”声明变量,比如,变量声明可以在代码块的中间,不过仍然必须在变量被使用前声明变量的名字和类型。在Python中,无需此类显式变量声明语句,变量在第一次被赋值时自动声明。和其他大多数语言一样,变量只有被创建和赋值后才能被使用。
变量一旦被赋值,你就可以通过变量名来访问它。
3.5.2 动态类型
还要注意一点,Python中不但变量名无需事先声明,而且也无需类型声明。在Python语言中,对象的类型和内存占用都是运行时确定的。尽管代码被编译成字节码,Python仍然是一种解释型语言。在创建——也就是赋值时,解释器会根据语法和右侧的操作数来决定新对象的类型。在对象创建后,一个该对象的应用会被赋值给左侧的变量。
3.5.3 内存分配
作为一个负责任的程序员,我们知道在为变量分配内存时,是在借用系统资源,在用完之后,应该释放借用的系统资源。Python解释器承担了内存管理的复杂任务,这大大简化了应用程序的编写。你只需要关心你要解决的问题,至于底层的事情放心交给Python解释器去做就行了。
3.5.4 引用计数
要保持追踪内存中的对象,Python使用了引用计数这一简单技术。也就是说Python内部记录着所有使用中的对象各有多少引用。你可以将它想像成扑克牌游戏“黑杰克”或“21点”。一个内部跟踪变量,称为一个引用计数器。每个对象各有多少个引用,简称引用计数。当对象被创建时,就创建了一个引用计数,当这个对象不再需要时,也就是说,这个对象的引用计数变为0时,它被垃圾回收。(严格来说这不是100%正确,不过现阶段你可以就这么理解)
1. 增加引用计数
当对象被创建并(将其引用)赋值给变量时,该对象的引用计数就被设置为1。
当同一个对象(的引用)又被赋值给其他变量时,或作为参数传递给函数、方法或类实例时,或者被赋值为一个窗口对象的成员时,该对象的一个新的引用,或者称作别名,就被创建(则该对象的引用计数自动加1)。
请看以下声明。
语句x=3.14创建了一个浮点型对象并将其引用赋值给x。x是第一个引用,因此,该对象的引用计数被设置为1。语句y=x创建了一个指向同一对象的别名y(参阅图3-2)。事实上并没有为Y创建一个新对象,而是该对象的引用计数增加了1次(变成了2)。这是对象引用计数增加的方式之一。还有一些其他的方式也能增加对象的引用计数,比如该对象作为参数被函数调用或这个对象被加入到某个容器对象当中时。
图3-2 有两个引用的同一对象
总之,对象的引用计数增加时:
- 对象被创建
- 或另外的别名被创建
- 或被作为参数传递给函数(新的本地引用)
- 或成为容器对象的一个元素
下面让我们来看一下引用计数是如何变少的。
2. 减少引用计数
当对象的引用被销毁时,引用计数会减小。最明显的例子就是当引用离开其作用范围时,这种情况最经常出现在函数运行结束时,所有局部变量都被自动销毁,对象的引用计数也就随之减少。
当变量被赋值给另外一个对象时,原对象的引用计数也会自动减1:
当字符串对象“xyz”被创建并赋值给foo时,它的引用计数是1。当增加了一个别名bar时,引用计数变成了2。不过当foo被重新赋值给整型对象123时,xyz对象的引用计数自动减1,又重新变成了1。
其他造成对象的引用计数减少的方式包括使用del语句删除一个变量(参阅稍后小节),或者当一个对象被移出一个窗口对象时(或该容器对象本身的引用计数变成了0时)。总结一下,一个对象的引用计数在以下情况下会减少。
一个本地引用离开了其作用范围。比如foobar()(参见刚才的例子)函数结束时。
对象的别名被显式销毁。
- 对象的一个别名被赋值给其他对象。
- 对象被从一个窗口对象中移除。
- 窗口对象本身被销毁。
参阅11.8节了解更多变量作用范围的信息。
3. del语句
Del语句会删除对象的一个引用,它的语法如下。
例如,在上例中执行del y会产生两个结果。
从现在的名称空间中删除y。
x的引用计数减1。
引申一步,执行del x会删除该对象的最后一个引用,也就是该对象的引用计数会减为0,这会导致该对象从此“无法访问”或“无法抵达”。从此刻起,该对象就成为垃圾回收机制的回收对象。注意任何追踪或调试程序会给一个对象增加一个额外的引用,这会推迟该对象被回收的时间。
3.5.5 垃圾收集
不再使用的内存会被一种称为垃圾收集的机制释放。像上面说的,虽然解释器跟踪对象的引用计数,但垃圾收集器负责释放内存。垃圾收集器是一块独立代码,它用来寻找引用计数为0的对象。它也负责检查那些虽然引用计数大于0但也应该被销毁的对象。特定情形会导致循环引用。
一个循环引用发生在当你有至少两个对象互相引用时,也就是说所有的引用都消失时,这些引用仍然存在,这说明只靠引用计数是不够的。Python的垃圾收集器实际上是一个引用计数器和一个循环垃圾收集器。当一个对象的引用计数变为0,解释器会暂停,释放掉这个对象和仅有这个对象可访问 (可到达)的其他对象。作为引用计数的补充,垃圾收集器也会留心被分配的总量很大的(及未通过引用计数销毁的那些)对象。在这种情况下,解释器会暂停下来,试图清理所有未引用的循环。