通过实例方法名字的字符串调用方法
getatter:获取
getatter(对象,属性【对象中有没有该属性】,None【可选填】)
字符串中使用find方法查找,有的话会返回下标,没有的话则-1。 使用getatter获取看是否有find方法。
当没有的方法会报错,此时,第三个参数可以使用None,当没有属性的时候返回None
已知:有三个图形类:Circle、Triangle、Rectangle
都有一个获取图形面积的方法,但是方法名字不同。
需求:实现一个统一获取面积的函数,使用每种方法名进行尝试,调用相应类的接口。
定义三个类,及三个不同名字的面积算法的方法名。
经典的参数错误
复习:
可变数据类型【内部内容可以发生变化】: 列表、字典
不可变数据类型【定义的内容可以取出但是不能改变】:字符串、数字、元组
可变数据类型:是在数据内部进行变化,所以数据的id 地址一致,没有改动
不可变数据类型:是将数据进行改动后重新赋值给变量,所以id地址会发生变化
图中函数中返回值a 在不可变数据类型中的表现为:重新创建了一个不可变类型对象
不可变数据的改动:
欧发生
可变数据类型的改动:
注意:
列表为可变类型:
- a += 1 相当于改变a本身
- a = a + 1 相当于a 是两个变量,id不一致,但返回的是原本的a的值
如图所示:
元组为不可变类型:
- a += 1 也就是重写创建一个a变量,id不一致
垃圾回收
内存与内存管理简介(了解)
内存
从计算机存储器的作用可分为:主存储器、辅助存储器、缓冲存储器。
主存储器也称为:主存、内存或 可执行存储器。是与cpu直接进行信息交换的存储器,读写速度相对较快、容量相对较小,通常用来保存进程运行时的程序和相应数据以供 cpu 使用。
辅助存储器也称为:辅存,不能与cpu直接进行信息交换,它的容量较大,读写速度相对较慢。
缓冲存储器:常用于两个速度不同的部件之间,比如 cpu和主存之间设置的高速缓冲存储 Cache(也可将内存的概念扩展为主存和高速缓冲存储器的合集)
操作系统的内存管理
内存容量有限,很难承载系统以及用户进程所需的全部信息,所以对操作系统需要对内存空间进行存储管理。大致功能包括:内存空间的划分与动态分配、回收、扩充 以及 存储保护、地址转换等等。
进程内的内存管理
操作系统对各个进程的内存进行管理,同时需要管理编写的程序所对应进程内的内存。从可用内存中申请内存并且具有足够内存来进行相关操作,以及适当的时间释放内存。
比如:打开qq软件可以看成是进程【正在运行的程序】。而使用qq和不同人聊天的窗口可以看成线程。
在c/c++ 中,需要手动进行内存管理,比如c语言中通过malloc 和free函数来申请给定字节数的内存以及释放对应的内存。 但在 python中,我们无需手动进行内存的申请和释放,python在内部帮我们完成了大量涉及到内存管理的操作,包括内存分配及垃圾回收。
内存分配
内存池机制
PyMalloc
在python中,有很多常用的数据结构:列表、字典等。比如在列表中,我们不仅可以保存其他不同类型的对象,而且可以非常方便的使用 append、 extend 等方法对其进行动态的扩充。对这些常用对象的一系列操作,会在python中造成内存的频繁分配和释放, 同时像 int、list等python对象的分配和释放通常涉及到的数据量相对较小,因此内部引入了内存池机制,实现小块内存的管理器(PyMalloc),用于提高处理小块内存的效率,避免底层频繁的malloc和free操作【c语言中】带来的效率影响。
分配策略
在内存分配中,python以512bytes 为界限对大内存和小内存进行分化,不超过512bytes 的内存申请,会通过PyMalloc管理器进行处理。超过512bytes的内存申请,则会通过c语言中的malloc来进行处理。
在管理器内部包括:block、pool、arena层级。其中,block是python内存管理中最小单元,一个pool中包含多个block,多个pool构成一个arena。同时,由于内存池机制,python不会讲释放分内存立刻归还操作系统,设置了缓冲池【比如小整数池】
缓冲池机制
提高常用对象的创建和释放效率,又进一步对整数、字符串等对象建立了对象缓冲池。比如对于[-5, 256]内的小整数创建的小整数对象缓冲池。
垃圾回收机制
介绍
python程序在运行时候,需要在内存中开辟出一块空间,用于存放运行时产生的临时变量,计算完成后再将结果输出到永久性存储器中。如果数据量过大,内存空间管理不善就容易出现OOM(out of memory),俗称爆内存,程序可能被操作系统终止。【比如我们说的死机。或者一个程序之前运行没问题但之后运行一些就崩溃、运行一下就崩溃这就是内存不断升高】
python中一切皆对象,因此,所看到的一切变量,本质都是对象的一个指针。当这个对象的引用计数(指针数)为0的时候,说明这个对象永不可达,成为了垃圾需要被回收。
os模块
与操作系统交互的库
psutil模块
与系统交互的库,能够轻松实现获取系统运行的进程和系统利用率(包括cpu、内存、磁盘、网络等)信息。 主要用来做系统监控、性能分析、进程管理。
运行时候如果出现下图中,没有权限的报错,可以使用管理员身份运行终端【命令提示符】
以管理员身份运行终端,在终端使用py文件,可以运行程序
根据上图发现创建列表a所消耗的内存最大,但是结束后的内存又变小了。这是因为a是函数内部的变量也就是局部变量,无法在函数外部使用。当函数返回之后,局部变量的引用会被消除,那么引用计数为0,就会和回收。
使用声明将局部变量为全局变量
变成全局变量,在方法引用后没被释放。
使用return返回值:
当return没有变量接收返回值当时候,还是被释放了。
没有变量接收返回值也就没有变量指向所以也被当成垃圾回收。当外部有变量接收return的时候就不会被释放
没有被释放,因为返回的值有变量指向了。但但返回的值没有变量接收也就是没有变量指向那么也会被当成垃圾回收。
函数中的局部变量不想被回收的方法:
方法1:
声明成为全局变量
方法2:
使用return并用变量接收返回值。
引用计数
引用sys模块, sys.getrefcount() 引用计数
图中正常引用计数为1.因为只有a 指向了空列表。但是得到的打印值为2。是因为getrefcount本身也要占用一次引用。
下图中的引用计数为4。
第一次:a =[ ] 即a本身。
第二次:函数的调用,即 func()调用a。
第三次:函数的参数,即在func()函数中形参接收调用的地方 。
第四次:getrefcount所占用的一次。
在a本身和getrefcount本身各占用一次后,每个指向a的都算作一次共有六个指向。所以一共8个。
手动启动垃圾回收
如果可以 手动删除完对象的引用 ,然后 强制调用gc.collect()清除没有引用的对象 ,其实也就是手动启动对象的回收。
gc.collect():手动清除没有引用的对象。
使用del 删除对象,那么引用指数就为0了,就无法被sys的getrefcount读取到就会报错。
要手动清除没有引用的对象首先要先删除对象的引用。否则内存效果不变,等于没有清除引用及对象。
清除引用和对象后内存得到释放如下图所示:
如果不删除对象的引用,也不清楚没有引用的对象。
那么没有被回收,内存没被释放
如果只删除对象的引用,而不手动清除对象那么得到的也是被释放的效果,因为python自动释放了没有指数的对象。
gc.collect是手动删除的。
循环引用
如果有两个对象,互相引用并且不再被别的对象所引用,那么引用计数还存在,但是为了预防不断循环引用引起oom 可以手动回收(gc.collect),进行释放内存。所以,引用计数为0 的时候不一定会被回收比如存在循环引用的情况。
如下:
按之前上文所说局部变量在函数调用结束后会自动被释放掉。【即函数结束后 打印的finished内存值应该减少,但是下图中结束后的内存值同函数运行时的值一致没有变动也就是说没有被释放。这就是我们所说的引用计数是垃圾回收的充分非必要条件,在计数为0的时候也不一定会被回收】
因为,我们在函数中设置了循环引用,也有引用环会出现这种情况【文件a引用了文件b, 文件b引用文件c,文件c引用文件d,之后文件d引用回文件a 形成一个引用环】
要解决循环引用不断占用的内存,就要使用手动清除引用,释放内存。gc.collect
查看a和b 的引用指数发现都是3次。且大致指向一致。
标记清除(Mark and Sweep)和分代回收(Generational)
为了解决引用计数在垃圾回收中无法处理循环引用的问题,python引入了标记清除和分代回收来检测和打破循环引用。
标记清除是追踪回收的基础算法。涉及到两个主要过程:标记过程、清除过程。
在标记过程中:将所有可达对象进行标记。
在清除过程中:将所有未标记的对象进行清除。
分代回收:在标记清除的基础上,一种空间换时间的实现策略。
分代回收分为3代。0代、 1代、2代。新创建的对象放入0代。若一个对象在经历过一次垃圾回收后没有被清除则进入下一代。对于每一代对象来说,都具有触发垃圾回收的相关阙值(收集频率)。
当在程序中确定不存在循环引用时可以用以下方法:
gc.get_threshold():获取当前回收阙值
gc.disable():关闭垃圾回收
不一定所有的内存都会被释放,比如全局变量、循环引用的某些对象等。可以使用gc.garbage()方法了解。
调试内存泄漏
python中通过引用计数和垃圾回收来管理内存,但在一定情况下也会产生内存泄漏[ 【代码设计好但是程序未能释放已经不能使用的内存】
- 对象被另一个生命周期特别长的对象所引用
- 循环引用中的对象定义了_ del_函数
objgraph
可视化引用关系包,包中的show_refs():可以生成清晰的引用关系图
.dot文件转图片:https://onlineconvertfree.com/
在pycharm中得到dot文件的路径,之后通过上面网页打开dot文件,转换后可以得到引用关系图。
将文件拖动到桌面进入之前 dot文件转换连接。选择图像。转换为png文件。
循环引用关系图、及文字说明解释:
用pdb进行代码调试
代码调试(查找代码运行时出现报错的地方或原因)有几种方法:
打印大法:在多个代码前输入print()方法。根据输入的print运行结果查看到错误地方。【此方法过慢,同时需要一层层的print调试】
debugger断点调试:
pdb调试:有时候python不是用pycharm书写而是其他软件可能没有Debugger断点调试、或者一些程序不是单一语言书写比如有python也有c+语言等。可以使用pdb调试。
pdb是自带的调试库,提供了交互式的源代码调试功能。
如何使用pdb
导入pdb模块,调用pdb.set_trace()
pdb.set_trace():类似debugger打断点调试。
根据pdb字母测试表来测试代码。下图中p指打印即print。 p a 为print(a)因为已经执行了所以能得到结果。
因为调试是从c=3开始准备运行的,但是还没运行,所以找不到c的值就会产生报错。
进入下一步是输入代码 n。此时进入了print(a+b+c)所以c=3已经执行完毕,可以打印c的值
pdb基本命令
- 进入命令行Debug模式,python -m pdb myscript.py
- h:(help)帮助
- w:(where)找出当前代码运行位置
- d:(down)返回下个调用点
- u:(up)返回上个调用点
- b:(break)添加断点b 列出当前所有断点,和断点执行到统计次数
b line_no:当前脚本的line_no行添加断点
b filename:line_no:脚本filename的line_no行添加断点
b function:在函数function的第一条可执行语句处添加断点
- tbreak:(temporary break)临时断点
在第一次执行到这个断点之后,就自动删除这个断点,用法和b一样 - cl:(clear)清除断点
cl 清除所有断点
cl bpnumber1 bpnumber2… 清除断点号为bpnumber1,bpnumber2…的断点
cl lineno 清除当前脚本lineno行的断点
cl filename:line_no 清除脚本filename的line_no行的断点
- disable:停用断点,参数为bpnumber,和cl的区别是,断点依然存在,只是不启用
- enable:激活断点,参数为bpnumber
- s:(step)执行下一条命令
如果本句是函数调用,则s会执行到函数的第一句 - n:(next)执行下一条语句
如果本句是函数调用,则执行函数,接着执行当前执行语句的下一条。 - r:(return)执行当前运行函数到结束
- c:(continue)继续执行,直到遇到下一条断点
- l:(list)列出源码
l 列出当前执行语句周围11条代码
l first 列出first行周围11条代码
l first second 列出first–second范围的代码,如果second<first,second将被解析为行数
- a:(args)列出当前执行函数的函数
- p expression:(print)输出expression的值
- pp expression:好看一点的p expression
- run:重新启动debug,相当于restart
- q:(quit)退出debug
- j lineno:(jump)设置下条执行的语句函数
只能在堆栈的最底层跳转,向后重新执行,向前可直接执行到行号 - unt:(until)执行到下一行(跳出循环),或者当前堆栈结束
- condition bpnumber conditon:给断点设置条件,当参数condition返回True的时候bpnumber断点有效,否则bpnumber断点无效
注意:
1:直接输入Enter,会执行上一条命令;
2:输入PDB不认识的命令,PDB会把他当做Python语句在当前环境下执行
用cProfile进行性能分析
工作中遇到:在线上,发现某个产品的某个功能模块效率低下、延迟高、占用的资源多,但是不知道问题在哪里。
此时对代码进行profile就很重要。
profile:对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。
python中递归最大深度为:
如果超过最大深度会报错提示超过最大深度。
对下图递归进行性能分析:
导入cProfile模块,使用cProfile.run()方法
cProfile.run(字符串形式的方法名或者需分析的性能对象名)
打印得到一个字段,结合参数介绍进行性能分析:
根据tottime的数值可以找到需要优化的代码部分进行优化。
参数介绍
- ncalls:函数被调用的次数。如果这一列有两个值,就表示有递归调用,第二个值是原生调用次数,第一个值是总调用次数。
- tottime:函数内部消耗的总时间。(可以帮助优化)
- percall:是tottime除以ncalls,一个函数每次调用平均消耗时间。
- cumtime:之前所有子函数消费时间的累计和。
- filename:lineno(function):被分析函数所在文件名、行号、函数名。