Python:垃圾回收机制&性能分析&经典的参数错误

Python什么时候才触发这个垃圾回收机制呢?
引用技术
当计数为0的时候,Python就会自动触发这个垃圾回收机制
Python默认的垃圾收集机制是“引用计数”,每个对象维护了一个ob_ref字段。它的优点是机制简单,当新的引用指向该对象时,引用计数加1,当一个对象的引用被销毁时减1,一旦对象的引用计数为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')

调试内存泄漏
虽然有了自动回收机制,但这也不是万能的,难免还是会有漏网之鱼。内存泄漏是我们不想见到的,而且还会严重影响性能。有没有什么好的调试手段呢?

import objgraph

a = [1, 2, 3]
b = [4, 5, 6]

a.append(b)
b.append(a)

objgraph.show_refs([a])

它就 是 objgraph,一个非常好用的可视化引用关系的包。在这个包中,我主要推荐两个函数,第一个是 show_refs(),它可以生成清晰的引用关系图。
总结

  • 垃圾回收是Python自带以及自动运行的机制,用于自动释放不会在用到内存空间
  • 引用技术是其中最简单的实现,不过切记,这只是充分非必要条件,因为循环引用需要通过不可达判定,来确定释放被回收了
  • Python的自动回收算法包括标记清除和分代收集,主要针对的是循环引用垃圾收集
  • 调试内存泄漏方面,objgraph是很好的可视化分析工具

调试和性能分析
用pdb进行代码调试
首先,我们来看代码的调试。也许不少人会有疑问:代码调试?说白了不就是在程序中使用 print() 语句吗?

没错,在程序中相应的地方打印,的确是调试程序的一个常用手段,但这只适用于小型程序。因为你每次都得重新运行整个程序,或是一个完整的功能模块,才能看到打印出来的变量值。如果程序不大,每次运行都非常快,那么使用 print(),的确是很方便的。

可能又有人会说,现在很多的 IDE 不都有内置的 debug 工具吗?
如何使用pdb
首先,要启动pdb调试,我们只需要在程序中,加入import pdbpdb.set_trace()这两行代码就行了

a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)

运行结果:

(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 的意思,即进入相对应的代码内部。
官方文档链接

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

运行结果:

3
1 2
[1, 2, 3, 4]
[1, 2, 3, 4] [3, 4]
(1, 2, 3, 4)
(1, 2) (3, 4)

重点:
不可变类型
以int类型为例:实际上 i += 1 并不是真的在原有的int对象上+1,而是重新创建一个value为6的int对象,i引用自这个新的对象。

可变类型
以list为例。list在append之后,还是指向同个内存地址,因为list是可变类型,可以在原处修改。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值