python垃圾回收机制
计数引用我们反复提过好几次, Python 中一切皆对象。因此,你所看到的一切变量,本质上都是对象的一个指针。
那么,怎么知道一个对象,是否永远都不能被调用了呢?
就是当这个对象的引用计数(指针数)为 0 的时候,说明这个对象永不可达,自然它也就成为了垃圾,需要被回收。
import os
import psutil
# 显示当前 python 程序占用的内存大小
def show_memory_info(hint):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss / 1024. / 1024
print('{} memory used: {} MB'.format(hint, memory))
def func():
show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after a created')
func()
show_memory_info('finished')
循环引用
如果有两个对象,它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收吗?
def func():
show_memory_info('initial')
a = [i for i in range(10000000)]
b = [i for i in range(10000000)]
show_memory_info('after a, b created')
a.append(b)
b.append(a)
func()
show_memory_info('finished')
调试内存泄漏
虽然有了自动回收机制,但这也不是万能的,难免还是会有漏网之鱼。内存泄漏是我们不想见到的,而且还会严重影响性能。有没有什么好的调试手段呢?
它就是 objgraph,一个非常好用的可视化引用关系的包。在这个包中,我主要推荐两个函数,第一个是 show_refs(),它可以生成清晰的引用关系图。
import objgraph
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
b.append(a)
objgraph.show_refs([a])
总结
垃圾回收是 Python 自带的机制,用于自动释放不会再用到的内存空间;
引用计数是其中最简单的实现,不过切记,这只是充分非必要条件,因为循环引用需要通过不可达判定,来确定是否可以回收;
Python 的自动回收算法包括标记清除和分代收集,主要针对的是循环引用的垃圾收集;
调试内存泄漏方面, objgraph 是很好的可视化分析工具。
调试和性能分析
用 pdb 进行代码调试
首先,我们来看代码的调试。也许不少人会有疑问:代码调试?说白了不就是在程序中使用 print() 语句吗?
没错,在程序中相应的地方打印,的确是调试程序的一个常用手段,但这只适用于小型程序。因为你每次都得重新运行整个程序,或是一个完整的功能模块,才能看到打印出来的变量值。如果程序不大,每次运行都非常快,那么使用 print(),的确是很方便的。
可能又有人会说,现在很多的 IDE 不都有内置的 debug 工具吗?
如何使用 pdb
首先,要启动 pdb 调试,我们只需要在程序中,加入import pdb和pdb.set_trace()这两行代码就行了
a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)
这时,我们就可以执行,在 IDE 断点调试器中可以执行的一切操作,比如打印,语法是"p ":
(pdb) p a
1
(pdb) p b
2
除了打印,常见的操作还有“n”,表示继续执行代码到下一行
(pdb) n
-> print(a + b + c)
而命令l,则表示列举出当前代码行上下的 11 行源代码,方便开发者熟悉当前断点周围的代码状态
(pdb) l
1 a = 1
2 b = 2
3 import pdb
4 pdb.set_trace()
5 -> c = 3
6 print(a + b + c)
命令“s“,就是 step into 的意思,即进入相对应的代码内部。
当然,除了这些常用命令,还有许多其他的命令可以使用
参考对应的官方文档:https://docs.python.org/3/library/pdb.html#module-pdb%EF%BC%89
用 cProfile 进行性能分析
关于调试的内容,我主要先讲这么多。事实上,除了要对程序进行调试,性能分析也是每个开发者的必备技能。
日常工作中,我们常常会遇到这样的问题:在线上,我发现产品的某个功能模块效率低下,延迟高,占用的资源多,但却不知道是哪里出了问题。
这时,对代码进行 profile 就显得异常重要了。
这里所谓的 profile,是指对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。
计算斐波拉契数列,运用递归思想
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
def fib_seq(n):
res = []
if n > 0:
res.extend(fib_seq(n-1))
res.append(fib(n))
return res
fib_seq(30)
接下来,我想要测试一下这段代码总的效率以及各个部分的效率
import cProfile
cProfile.run('fib_seq(30)')
参数介绍:
- ncalls,是指相应代码 / 函数被调用的次数
- tottime,是指对应代码 / 函数总共执行所需要的时间(注意,并不包括它调用的其他代码 / 函数的执行时间)
- tottime percall,就是上述两者相除的结果,也就是tottime / ncalls
- cumtime,则是指对应代码 / 函数总共执行所需要的时间,这里包括了它调用的其他代码 / 函数的执行时间
- cumtime percall,则是 cumtime 和 ncalls 相除的平均结果。
经典参数错误
def add(a,b):
a += b
return a
a = 1
b = 2
c = add(a,b)
print(c)
print(a,b)
a = [1,2]
b = [3,4]
c = add(a,b)
print(c)
print(a,b)
a = (1,2)
b = (3,4)
c = add(a,b)
print(c)
print(a,b)
不可变类型
以int类型为例:实际上 i += 1 并不是真的在原有的int对象上+1,而是重新创建一个value为6的int对象,i引用自这个新的对象。
可变类型
以list为例。list在append之后,还是指向同个内存地址,因为list是可变类型,可以在原处修改。
os 模块
与操作系统交互的库
psutil模块
与系统交互的库,能够轻松实现获取系统运行的进程和系统利用率(包括CPU、内存、磁盘、网络等)信息。它主要用来做系统监控,性能分析,进程管理。
通过以下代码检测程序在运行时的内存消耗
import os
import psutil
def show_info(start):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss/1024./1024
print(f"{start}一共占用{memory:.2f}MB")
def func():
show_info("initial")
a = [i for i in range(1000000)]
show_info("created")
func()
show_info("finished")
- 当a是局部变量时,在返回到函数调用处时,局部变量的引用会注销。这时,列表a所指代对象的引用数为0,Python便会执行垃圾回收,因此之前占用的内存被收回了。
- 当a是全局变量的时,即使函数体内代码执行完毕,返回到函数调用处时,对列表a的引用仍然是存在的,所以对象不会被垃圾回收,依然占有大量内存。
Python内部的引用计数机制
我们可以通过sys.getrefcount()这个函数,来了解Python内部的引用计数机制。
import sys
a = [1,2,3]
print(sys.getrefcount(a))
- getrefcount本身也会引入一次计数。
手动启动垃圾回收
如果我们可以手动删除完对象的引用,然后强制调用gc.collect()清除没有引用的对象,其实也就是手动的启动对象的回收。
循环引用
如果有两个对象,它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收
import gc
def show_info(start):
pid = os.getpid()
p = psutil.Process(pid)
info = p.memory_full_info()
memory = info.uss/1024./1024
print(f"{start}一共占用{memory:.2f}MB")
def func():
show_info("initial")
a = [i for i in range(100000)]
b = [i for i in range(100000)]
show_info("after a,b created")
# 相互引用
a.append(b)
b.append(a)
func()
gc.collect()
show_info("finished")