- Preface 前言
- 第1章 引论
- 第2章 编程惯用法
- 第3章 基础语法
- 建议19:有节制地使用 from…import 语句
- 建议20:优先使用 absolute import 来导入模块
- 建议21:i+=1 不等于 ++i
- 建议22:使用 with 自动关闭资源
- 建议23:使用 else 子句简化循环(异常处理)
- 建议24:遵循异常处理的几点基本原则
- 建议25:避免 finally 中可能发生的陷阱
- 建议26:深入理解 None 正确判断对象是否为空
- 建议27:连接字符串应优先使用 join 而不是 +
- 建议28:格式化字符串时尽量使用 .format 方式而不是 %
- 建议29:区别对待可变对象和不可变对象
- 建议30:[]、() 和 {}:一致的容器初始化形式
- 建议31:记住函数传参既不是传值也不是传引用
- 建议32:警惕默认参数潜在的问题
- 建议33:慎用变长参数
- 建议34:深入理解 str() 和 repr() 的区别
- 建议35:分清 staticmethod 和 classmethod 的适用场景
- 第4章 库
- 建议36:掌握字符串的基本用法
- 建议37:按需选择 sort() 或者 sorted()
- 建议38:使用 copy 模块深拷贝对象
- 建议39:使用 Counter 进行计数统计
- 建议40:深入掌握 ConfigParser
- 建议41:使用 argparse 处理命令行参数
- 建议42:使用 pandas 处理大型 CSV 文件
- 建议43:一般情况使用 ElementTree 解析 XML
- 建议44:理解模块 pickle 优劣
- 建议45:序列化的另一个不错的选择 JSON
- 建议46:使用 traceback 获取栈信息
- 建议47:使用 logging 记录日志信息
- 建议48:使用 threading 模块编写多线程程序
- 建议49:使用 Queue 使多线程编程更安全
- 第5章 设计模式
- 第6章 内部机制
- 建议54:理解 built-in objects
- 建议55:init() 不是构造方法
- 建议56:理解名字查找机制
- 建议57:为什么需要 self 参数
- 建议58:理解 MRO 与多继承
- 建议59:理解描述符机制
- 建议60:区别 getattr() 和 getattribute() 方法
- 建议61:使用更为安全的 property
- 建议62:掌握 metaclass
- 建议63:熟悉 Python 对象协议
- 建议64:利用操作符重载实现中缀语法
- 建议65:熟悉 Python 的迭代器协议
- 建议66:熟悉 Python 的生成器
- 建议67:基于生成器的协程及 greenlet
- 建议68:理解 GIL 的局限性
- 建议69:对象的管理与垃圾回收
- 第7章 使用工具辅助项目开发
- 第8章 性能剖析与优化
建议69:对象的管理与垃圾回收
通常来说Python并不需要用户自己来管理内存,它与Perl、Ruby等很多动态语言一样具备垃圾回收功能,可以自动管理内存的分配与回收,而不需要编程人员的介入。那么这样是不是意味着用户可以高枕无忧了呢?我们来看下面一个例子:
class Leak(object): def __init__(self): print "object with id %d was born" %id(self) while(True): A = Leak() B = Leak() A.b = B B.a = A A = None B = None
运行上述程序我们会发现,Python进程的内存消耗量一直在持续增长,到最后出现内存耗光的情况。这是什么原因造成的呢?
我们先来简单谈谈Python中内存管理的方式:Python使用引用计数器(Reference counting)的方法来管理内存中的对象,即针对每一个对象维护一个引用计数值来表示该对象当前有多少个引用。当其他对象引用该对象时,其引用计数会增加1,而删除一个对当前对象的引用,其引用计数会减1。只有当引用计数的值为0的时候该对象才会被垃圾收集器回收,因为它表示这个对象不再被其他对象引用,是个不可达对象。引用计数算法最明显的缺点是无法解决循环引用的问题,即两个对象相互引用。上述代码中正是由于形成了A、B对象之间的循环引用而造成了内存泄露的情况,因为两个对象的引用计数器都不为0,该对象并不会被垃圾收集器回收,而无限循环导致一直在申请内存而没有释放,所以最后出现了内存耗光的情况。
循环引用常常会在列表、元组、字典、实例以及函数使用时出现。对于由循环引用而导致的内存泄露的情况,有没有办法进行控制和管理呢?实际上Python自带了一个gc模块,它可以用来跟踪对象的“入引用(incoming reference)”和“出引用(outgoing reference)”,并找出复杂数据结构之间的循环引用,同时回收内存垃圾。有两种方式可以触发垃圾回收:一种是通过显式地调用gc.collect()进行垃圾回收;还有一种是在创建新的对象为其分配内存的时候,检查threshold阈值,当对象的数量超过threshold的时候便自动进行垃圾回收。默认情况下阈值设为(700,10,10),并且gc的自动回收功能是开启的,这些可以通过gc.isenabled()查看。下面是gc模块使用的简单例子:
>>> import gc >>> print gc.isenabled() True >>> gc.isenabled() True >>> gc.get_threshold() (700, 10, 10)
对于本节开头的例子,我们使用gc模块来进行垃圾回收,代码如下:
def main(): collected = gc.collect() print "Garbage collector before running: collected %d objects." % (collected) print "Creating reference cycles..." A = Leak() B = Leak() A.b = B B.a = A A = None B = None collected = gc.collect() pprint.pprint( gc.garbage) print "Garbage collector after running: collected %d objects." % (collected) if __name__ == "__main__": ret = main() sys.exit(ret)
运行程序输出结果如下:
Garbage collector before running: collected 0 objects. Creating reference cycles... object with id 14109584 was born object with id 14109648 was born [] Garbage collector after running: collected 4 objects.
gc.garbage返回的是由于循环引用而产生的不可达的垃圾对象的列表,输出为空表示内存中此时不存在垃圾对象。gc.collect()显示所有收集和销毁的对象的数目,此处为4(2个对象A、B,以及其实例属性dict)。
我们再来考虑一个问题:如果我们在类Leak中添加析构方法__del__(),对象的销毁形式和内存回收的情况是否有所不同。示例代码如下:
def __del__(self): print "object with id %d was destoryed" %id(self)
当加入了析构方法__del__()在运行程序的时候会发现gc.garbage的输出不再为空,而是对象A、B的内存地址,也就是说这两个对象在内存中仍然以“垃圾”的形式存在。gc.garbag()输出如下:
[<__main__.Leak object at 0x00D72BF0>, <__main__.Leak object at 0x00D72C30>]
这是什么原因造成的呢?实际上当存在循环引用并且当这个环中存在多个析构方法时,垃圾回收器不能确定对象析构的顺序,所以为了安全起见仍然保持这些对象不被销毁。而当环被打破时,gc在回收对象的时候便会再次自动调用__del__()方法。读者可以自行试验。
gc模块同时支持DEBUG模式,当设置DEBUG模式之后,对于循环引用造成的内存泄露,gc并不释放内存,而是输出更为详细的诊断信息为发现内存泄露提供便利,从而方便程序员进行修复。更多gc模块的使用方法读者可以参考文档:http://docs.python.org/2/library/gc.html。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论