高级编程技巧——Python 垃圾回收及性能分析
Python 垃圾回收及性能分析
3.1 通过实例方法名字的字符串调用方法
- 我们有三个图形 Circle,Triangle,Rectange
- 他们都有一个获取面积的方法,但是方法名字不同,我们可以实现一个统一的获取面积的函数,使用每种方法名进行尝试,调用相应的接口。
class Triangle:
def __init__(self,a,b,c):
self.a,self.b,self.c=a,b,c
def get_area(self):
a,b,c= self.a,self.b,self.c
p=(a+b+c)/2
return (p*(p-a)*(p-b)*(p-c))**0.5
class Rectangle:
def __init__(self,a,b):
self.a, self.b=a,b
def getArea(self):
return self.a *self.b
class Circle
def __init__(self,r)
self.r=r
def area(self):
return self.r **2 ** 3.14159
- getattr(x,‘y’,None)—>等同于 x.y 当中不含有y时,返回None。
- map(func,iterable)—> 将iterable 中的元素一一映射到func函数中处理,并且返回新的map对象。
3.2 垃圾回收机制
介绍
- 在Python 程序运行的时候,会在内存中开辟一块空间,用于存放临时变量;当计算完成之后,就会将结果输出到永久存储器中,如果数据量特别大,那内存空间管理部妥当的话就非常容易爆内存,程序可能直接终止。
- 在Python中,一切皆对象。所以,每一个变量,实际上都是对象的一个指针。所以,当这个对象的引用计数(指针数)为0的时候,说明它也变成了垃圾,需要被放到回收箱中。
OS模块
- 与操作系统交互的库
psutill模块
- 与系统交互的库,能够轻松实现获取系统运行的进程和系统利用率(包括CPU、内存、磁盘、网络等)信息,它是用来做系统监控、性能分析、进程管理。
import os
import psutil
def show_info(start):
# 获取当前进程id
pid=os.getpid()
# 获取当前堆成对象
p=psutil.Process(pid)
# 获取进程独自占用的物理内存 换算单位MB
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')
import os
import psutil
def show_info(start):
# 获取当前进程id
pid=os.getpid()
# 获取当前堆成对象
p=psutil.Process(pid)
# 获取进程独自占用的物理内存 换算单位MB
info=p.memory_full_info()
memory=info.uss/1024./1024
print(f'{start}一共占用{memory:2f}MB')
def func():
show_info('initial')
global a # a是局部变量,用global 声明它是全局变量
a=[i for i in range(1000000)]
show_info('created')
func()
show_info('finished')
import psutil
def show_info(start):
# 获取当前进程id
pid=os.getpid()
# 获取当前堆成对象
p=psutil.Process(pid)
# 获取进程独自占用的物理内存 换算单位MB
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')
return a
a=func()
show_info('finished')
- 当a是局部变量时,在返回到函数调用处时,局部变量的引用会注销。这时,列表a所指代对象的引用数为0,Python便会执行垃圾回收,因此之前占用的内存被收回了。
- 当a是全局变量时,即使函数体内代执行完毕,返回到函数调用处时,对列表a的引用仍然是存在的,所以对象不会被垃圾回收,仍然占用大量内存。
Python 内部的引用计数机制
- 可以通过sys.getrefcount() 这个函数,来了解**Python内部的引用计数机制。
import sys
a=[1,2,3]
# print(sys.getrefcount(a)) # a这里引用了 2 次
def func(a):
# 这里调用a 4次
# a本身一次 函数调用1次 函数参数一次 getrefcount 一次
print(sys.getrefcount(a))
func(a)
import sys
a=[1,2,3]
def func(a):
print(sys.getrefcount(a))
func(a)
# a作为形参相当于函数体内的临时变量 所以 调用执行完毕会被释放掉 引用次数为0
# 2 a 本身一次 getrefcount 一次
print(sys.getrefcount(a))
import sys
a=[1,2,3]
print(sys.getrefcount(a)) # 2
b=a
print(sys.getrefcount(a)) # 3
c=b
d=c
print(sys.getrefcount(a)) # 5
# 1.getrefcount() 只计一次运用次数
# 2.变量赋值 b变量指向了 a所在内存地址
- getrefcount 本身也会引入一次计数
手动启动垃圾回收
- 如果我们可以在手动删除完对象的引用,然后强调用gc.collect()清除没有引用的对象,其实也就是手动的启动对象的回收。
import sys
import gc
a = [1,2,3]
print(sys.getrefcount(a))
# a=None # 相当于将a 变量指向了 None
del a # 相当于自己把对象的引用删掉 本质上对象还没有被删除
gc.collect() # 手动启动回收
print(a) # NameError: name 'a' is not defined 此时a 已经被回收
import sys
import gc
a = 1 # 小整数对象池
b=10000
print(sys.getrefcount(a)) # 134
print(sys.getrefcount(b)) # 4
循环引用
- 如果有两个对象,他们互相引用,并且不再被别的对象引用,那么它们应该被垃圾回收吗?
import os
import psutil
import gc
'''
引用次数 为0 的时候 一定会启用 垃圾回收吗
垃圾回收 一定是引用次数 为0的时候吗
充分必要条件
'''
def show_info(start):
# 获取当前进度id
pid=os.getpid()
# 获取当前堆成对象
p=psutil.Process(pid)
# 返回该对象的内存消耗
info = p.memory_full_info()
# 获取进程独自占用的物理内存 换算单位 MB
memory=info.uss/1024/1024
print(f'{start}一共占用{memory:2f}MB')
def func():
show_info('initial')
a=[i for i in range(1000000)]
b=[i for i in range(1000000)]
show_info('created')
# 相互引用
a.append(b)
b.append(a)
a=func()
gc.collect() # 手动回收 如果引用次数不为0时 手动回收 也是可以的
show_info('finished')
- 总而言之,当双向引用的时候,引用计数虽然还在,但我们可以手动拉起来回收,进行释放内存,所以,引用次数是垃圾回收的充分非必要条件
调试内存泄漏
- 在Python 中通过引用计数和垃圾回收来管理内存,但是在一定情况下也会产生内存泄露
- 第一是对象被另一个生命周期特别长得对象所引用
- 第二是循环引用中的对象定义了 __del __函数
objgraph,一个非常好用的可视化引用关系的包。在这个包中的 show_refs(),它可以清晰的引用关系图。
import objgraph
a=[1,2,3]
b=[4,5,6]
a.append(b)
b.append(a)
objgraph.show_refs(a)
- .dot 文件转图片:https://onlineconvertfree.com/
3.3 用 pdb 进行代码调试
如何使用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)
- 而命令!,则表示列举出当前代码行上下的11行源代码,方便开发者熟悉当前断点周围的代码状态
(pdb)1
a=1
b=2
import pdb
pdb.set_trace()
-> c=3
print(a+b+c)
- 命令 ‘s’ ,就是step into 的意思,即进入相应对应的代码内部
- 当然,除了这些常用命令,还有许多其它的命令可以调用
- 参考对应的官方文档:https//docs.python.org/3/library/pdb.htm|#module-pdb)
3.4 用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)') # fib_seq(30) 必须为字符串
参数介绍
- ncalls: 函数调用的次数,如果这一有两个值,就表示有递归调用,第二个值是原生调用次数,第一个值是总调用次数。
- tottime: 函数内部消耗的总时间。(可以帮助优化)
- percall: 是tottime 除以ncalls,一个函数每次调用平均消耗时间
- cumtime:之前所有子函数消费时间的累积和。
- filename: lineno(function): 被分析函数所在文件名,行号,函数名。
3.5 经典的参数错误
def add(a,d):
a+=b
return a
a=1
b=2
c=add(a,b)
print(c) # 3
print(a,b) # 1 2
def add(a,d):
a+=b
return a
a=[1,2]
b=[3,4]
c=add(a,b)
print(c) # [1, 2, 3, 4]
print(a,b) # [1, 2, 3, 4] [3, 4] 可变的数据类型
注意
- 列表为可变类型
- li+=1 相当于改变li本身
- li =li+1 相当于li 是两个变量 id 不一致 返回的是 原本的li
- 元组为不可变类型
- tu +=1 也就是重新创建了一个 tu 变量 id 不一致。