Day3 PythonWeb全栈课程课堂内容
1. 经典的参数错误
- 在经典的计算当中,有着许多参数运算的规则。
'''例子1'''
def add(a,b):
a += b
return a
a = 1
b = 2
c = add(a, b)
print(a, b, c)
'''
结果:a = 1 ; b = 2 ; c = 3
'''
- 从例子1中我们发现 a 出现在好几个地方,**def add(a,b) ; return a ; a += b ; a = 1 ; c = add(a,b)**中,这次我们主要看add()函数中的a和a=1的变化情况,从最后的结果可以看出,实参a=1,a的值未发生变化。而在add()的函数当中,a的值发生了改变。我们进一步进行验证add()函数中发生了变化。
def add(a,b):
a += b
print(a) # 3
return a
a = 1
b = 2
c = add(a, b)
print(a, b, c) # 1, 2, 3
- 你可从中发现a在add()函数中的值发生了改变,而最后的a值未发生变化,与实参a=1相同。
- 接下来我们在做几个例子:
'''例子2'''
def add(a,b):
a += b
print(a) # [1, 2, 3, 4]
return a
a = [1, 2]
b = [3, 4]
c = add(a, b)
print(a, b, c) # [1, 2, 3, 4], [3, 4], [1, 2, 3, 4]
'''例子3'''
def add(a,b):
a += b
print(a) # (1, 2, 3, 4)
return a
a = (1, 2)
b = (3, 4)
c = add(a, b)
print(a, b, c) # (1, 2), (3, 4), (1, 2, 3, 4)
- 从例子2中我们惊奇的发现a的值在最后发生了改变,而例子3中a的值未发生改变。这是为什么呢?
- 原因:因为在Python有两种可变类型,分为不可变类型,和可变类型。
类型 | 具体类型 |
---|---|
不可变类型 | 元组,字符串,整数… |
可变类型 | 字典,集合,数列… |
- 由于数组为可变类型,而元组为不可变类型,我们不难发现例子2中的a发生变化,而例子3中的a未发生变化。
- 我们还有不同的解释,获取a的地址,我们依旧使用例子2和例子3。
'''例子2'''
def add(a,b):
a += b
print(id(a)) # 197620424
return a
a = [1, 2]
print(id(a)) # 197620424
b = [3, 4]
c = add(a, b)
print(id(a)) # 197620424
'''例子3'''
def add(a,b):
a += b
print(id(a)) # 197450072
return a
a = (1, 2)
print(id(a)) # 197464584
b = (3, 4)
c = add(a, b)
print(id(a)) # 197464584
- 不难发现例子2中a的id地址不变,使得a的值发生变化。而例子3中a的id地址发生改变,add()中a指向197450072地址的值,结束后a又指向197464584,使得a的值不发生变化。
- 我们再看看下面这个例子。
'''例子4'''
def add(a,b):
a = a + b
print(a) # [1, 2, 3, 4]
print(id(a)) # 197620744
return a
a = [1, 2]
print(id(a)) # 197620424
b = [3, 4]
c = add(a, b)
print(a) # [1, 2]
print(id(a)) # 197620424
- 从例子4中发现,add()函数中,将原来的a += b改成a = a + b,最后a的值发生了未改变,因为add()函数中a的地址发生改变,所以最后a的未发生改变。
总结:
- 列表为可变类型
- list += 1,相当于改变list本身。
- list = list + 1,相当于list是两个变量,id不一致,返回的是,原本的list
- 元组为不可变类型
- tuple += 1 也就是重新创建了一个tuple变量,id不一致,返回的是,原本的tuple
2. 內存与内存管理的简介
- 何为内存?
- 从计算机存储器的作用来讲,存储器可以分为主存储器,辅助存储器和缓冲存储器。
- 其中主存储器,也称主存、内存或可执行存储器,是与 CPU 直接进行信息交换的存储器,它的读写速度相对较快,容量相对较小,通常用来保存进程运行时的程序和相应数据以供 CPU 使用;而辅存不能与 CPU 直接进行信息交换,它的容量较大,读写速度相对较慢。缓冲存储器常用于两个速度不同的部件之间,比如 CPU 和主存之间设置的高速缓冲存储 Cache(也可将内存的概念扩展为主存和高速缓冲存储器的合集)。
-
操作系统的内存管理
- 由于内存的容量有限,很难承载系统以及用户进程所需的全部信息,所以操作系统需要对内存空间进行存储管理。在操作系统层面上内存管理的实现相当复杂,其大致功能包括内存空间的划分与动态分配、回收、扩充,以及存储保护,地址转换等等。
-
进程内的内存管理
-
操作系统对各个进程的内存进行管理,同时我们也需要管理我们编写的程序所对应进程内的内存。从可用内存中申请内存并且具有足够内存来进行相关操作,以及在适当的时间释放内存,这些都是我们的程序和系统能够正常运行的前提。
-
在 C / C++ 中,我们需要手动的进行内存管理,比如 C 语言中通过
malloc
和free
函数来申请给定字节数的内存以及释放对应的内存。但在 Python 中,我们无须手动进行内存的申请和释放,Python 在内部帮我们完成了大量涉及到内存管理的操作,包括内存分配及垃圾回收。
-
3. 内存分配
-
内存池机制
-
什么是 PyMalloc ?
-
在 Python 中,有很多常用的数据结构,包括列表、字典等。比如在列表中,我们不仅可以保存其他不同类型的对象,而且可以非常方便的使用
append
、extend
等方法对其进行动态的扩充。针对这些常用对象的一系列操作,会在 Python 中造成内存的频繁分配和释放,同时像int
、list
等 Python 对象的分配和释放通常涉及到的数据量相对较小,因此 Python 在内部引入了内存池机制,实现了小块内存的管理器(称为 PyMalloc )用于提高处理小块内存的效率,这样避免了在底层频繁的malloc
和free
操作对效率带来的影响。 -
分配策略
-
在内存分配中,Python 以 512 bytes 为界限对大内存和小内存进行划分,不超过 512 bytes 的内存申请,会通过 PyMalloc 管理器进行处理,超过 512 bytes 的内存申请,则会通过 C 中的
malloc
来进行处理。在管理器的内部,主要包括block
、pool
、arena
层级, 其中block
是 Python 内存管理中的最小单元,一个pool
中包含多个block
,多个pool
构成一个arena
。 同时,由于内存池机制,Python 并不会将释放的内存立即归还操作系统。
-
-
缓冲池机制
- 在内存池机制的基础之上,Python 为了提高常用对象的创建和释放效率,又进一步对整数、字符串等对象建立了对象缓冲池。比如对于 [-5, 256] 内的小整数,Python 已经在内部创建好了对应的小整数对象的缓冲池。
4. 垃圾回收机制
-
垃圾回收机制
-
介绍
-
我们知道,Python 程序在运行的时候,需要在内存中开辟出一块空间,用于存放运行时产生的临时变量;计算完成后,再将结果输出到永久性存储器中。如果数据量过大,内存空间管理不善就很容易出现 OOM(out of memory),俗称爆内存,程序可能被操作系统中止。
-
Python 中一切皆对象。因此,你所看到的一切变量,本质上都是对象的一个指针。
-
那么,怎么知道一个对象,是否永远都不能被调用了呢?
-
就是当这个对象的引用计数(指针数)为 0 的时候,说明这个对象永不可达,自然它也就成为了垃圾,需要被回收。
-
-
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") ''' initial一共占用27.71MB created一共占用66.55MB finished一共占用27.96MB a没有指向,被内存释放掉,使最后内存减少。 '''
- a不被释放的情况
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") global a a = [i for i in range(1000000)] show_info("created") func() show_info("finished") ''' initial一共占用27.66MB created一共占用66.62MB finished一共占用66.62MB 当把a设置成全局变量时,你会发现,最后内存并没有被释放,原因是a被调用,指向了一个值。 '''
- 另外一种情况也是a被一直使用。
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") return a a = func() show_info("finished") ''' initial一共占用27.54MB created一共占用66.50MB finished一共占用66.50MB 返回a 并且a指向返回值,是的内存没有释放,内存使用不变。 '''
- 而当返回值不指向时,你看:
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") return a func() show_info("finished") ''' initial一共占用27.66MB created一共占用66.62MB finished一共占用27.77MB 由于不是调用返回值,并不被指向,内存释放,内存使用下降。 '''
总结
-
当a是局部变量时,在返回到函数调用处时,局部变量的引用会注销。这时,列表a所指代对象的引用数为0,Python便会执行垃圾回收,因此之前占用的内存被收回了。
-
当a是全局变量的时,即使函数体内代码执行完毕,返回到函数调用处时,对列表a的引用仍然是存在的,所以对象不会被垃圾回收,依然占有大量内存。
5. 引用计数
- 使用sys模块
import sys
a = []
print(sys.getrefcount(a))
# getrefcount() 本身也要占用一次
# 2 表示a被调用2次。
- 接下来一个例子中
a = [] # 第一次
def func(a): # 第三次
print(sys.getrefcount(a)) # 第四次
func(a) # 第二次
# 4 表示a被调用4次。
a = [] # 第一次
b = a # 第二次
c = b # 第三次
e = c # 第四次
print(sys.getrefcount(a)) # 第五次
# 5
5. 手动启动启动垃圾回收
- gc 模块
import os
import psutil
import gc
import sys
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")
show_info("initial")
a = [i for i in range(1000000)]
show_info("created")
del a
get(sys.getrefcount(a))
'''
get(sys.getrefcount(a))
NameError: name 'a' is not defined
经过del a 之后,a 被删除,没有被引用
所以不能用sys.getrefcount()来查看使用情况。
'''
- 选择其他方法
import os
import psutil
import gc
import sys
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")
show_info("initial")
a = [i for i in range(1000000)]
show_info("created")
del a # 删除a对象的引用
gc.collect() #删除没有引用的对象。 手动删除。
show_info("finished")
'''
initial一共占用27.50MB
created一共占用66.47MB
finished一共占用27.62MB
'''
- 就算不执行gc.collect(),python也会自动帮你释放。
6. 循环引用
-
引用计数为0的时候,该对象一定会被回收吗?
-
举个案例:
import gc
import psutil
import os
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()
show_info("finished")
'''
initial一共占用27.65MB
after a,b created一共占用35.91MB
finished一共占用35.91MB
出现一个引用环。python没有自动释放,使得内存最后没有被释放
'''
- 出现一个引用环。python没有自动释放,使得内存最后没有被释放,所以需要用到手动释放功能,gc.collect()
import gc
import psutil
import os
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")
'''
initial一共占用27.66MB
after a,b created一共占用35.91MB
finished一共占用27.74MB
'''
- 使用手动删除,使得内存占用下降。
import gc
import psutil
import os
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)
print(sys.getrefcount(a)) # 3
print(sys.getrefcount(b)) # 3
func()
gc.collect()
show_info("finished")
'''
initial一共占用27.66MB
after a,b created一共占用35.91MB
finished一共占用27.74MB
'''
- 总而言之,当双向引用的时候,引用计数虽然还在,但我们可以手动拉起回收,进行释放内存。所以,引用次数是垃圾回收的 充分非必要条件。
-
python中解决循环引用的方法
-
标记清除(Mark and Sweep)和分代回收(Generational)
- 为了解决引用计数在垃圾回收中无法处理循环引用的问题,Python 引入了标记清除和分代回收来检测和打破循环引用 。相对准确的说,标记清除是追踪回收( Tracing garbage collection )中的一种基础算法,其涉及到两个主要过程,即标记过程和清除过程,在标记过程中将所有可达(reachable)对象进行标记,在清除过程中将所有未标记的对象进行清除。分代回收则是基于标记清除基础上的,一种空间换时间的实现策略。
-
分代回收
- 分代回收将 Python 对象划分成 3 代,包括 0、1、2 代。对于新创建的对象,会被放入 0 代。若一个对象在经过一次垃圾回收后没有被清除,则它会被放入下一代中。对于每一代对象来说,都具有触发垃圾回收的相关阈值(收集频率)。关于这个过程的细节,官方文档中给出了比较明确的描述:
垃圾回收器把所有对象分类为三代,取决于对象幸存于多少次垃圾回收。新创建的对象会被放在最年轻代(第
0
代)。如果一个对象幸存于一次垃圾回收,则该对象会被放入下一代。第2
代是最老的一代,因此这一代的对象幸存于垃圾回收后,仍会留在第2
代。为了判定何时需要进行垃圾回收,垃圾回收器会跟踪上一次回收后,分配和释放的对象的数目。当分配对象的数量减去释放对象的数量大于阈值 threshold0 时,回收器开始进行垃圾回收。起初只有第
0
代会被检查。当上一次第1
代被检查后,第0
代被检查的次数多于阈值 threshold1 时,第1
代也会被检查。相似的, threshold2 设置了触发第2
代被垃圾回收的第1
代被垃圾回收的次数。
- 分代回收的主要目的是降低回收中需要处理的对象数量,提高垃圾回收效率。我们可以使用
gc
模块来执行或优化垃圾回收的相关过程,并获取更多的调试信息,比如通过gc.get_threshold()
来获取当前的回收阈值;通过gc.disable()
关闭垃圾回收,这通常在程序中确定不存在循环引用时使用。 - 另外,一个常见的问题是 CPython 在退出时一定会释放所有内存吗?答案是否定的,当 Python 解释器退出时,会进行内存清理,试图释放每个对象的内存,但不一定会释放所有内存,比如全局命名空间中引用的某些对象、循环引用下 C 扩展库中分配的某些内存都有可能不被释放(一部分相关细节可以通过
gc.garbage
了解)。
7. 调试泄露
-
对内存分配的浪费。
-
在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)
'''
Graph written to C:\Users\ADMINI~1\AppData\Local\Temp\objgraph-8rl1e1f2.dot (7 nodes)
Graph viewer (xdot) and image renderer (dot) not found, not doing anything else
'''
- 通过objgraph会生成一个后缀名为.dot的文件。普通文件是打不开的,例如用word打开是这个内容
digraph ObjectGraph {
node[shape=box, style=filled, fillcolor=white];
o1612344336[fontcolor=red];
o1612344368[fontcolor=red];
o1612344400[fontcolor=red];
o196335752[fontcolor=red];
o1612344336[label="int\n1"];
o1612344336[fillcolor="0,0,1"];
o1612344368[label="int\n2"];
o1612344368[fillcolor="0,0,1"];
o1612344400[label="int\n3"];
o1612344400[fillcolor="0,0,1"];
o196335752[label="list\n4 items"];
o196335752[fillcolor="0,0,1"];
o196335752 -> o1612344496;
o196335752 -> o1612344464;
o196335752 -> o1612344432;
o1612344496[label="int\n6"];
o1612344496[fillcolor="0,0,0.766667"];
o1612344464[label="int\n5"];
o1612344464[fillcolor="0,0,0.766667"];
o1612344432[label="int\n4"];
o1612344432[fillcolor="0,0,0.766667"];
}
import objgraph
a = [1,2,3]
b = [4,5,6]
a.append(b)
b.append(a)
objgraph.show_refs([a])
'''
Graph written to C:\Users\ADMINI~1\AppData\Local\Temp\objgraph-nlqf7rua.dot (8 nodes)
Graph viewer (xdot) and image renderer (dot) not found, not doing anything else
'''
import objgraph
a = [1,2,3]
b = [4,5,6]
a.append(b)
b.append(a)
objgraph.show_refs([b])
- 使用 objgraph 工具查看。
8. 代码调试
- 必须使用的方法
a = 1
b = 2
def demo():
1/0
demo()
c = 3
'''
1 / 0
ZeroDivisionError: division by zero
'''
-
会出现代码错误如何调试。
- 使用print方法定位。
a = 1 b = 2 print(1) def demo(): 1/0 print(2) demo() print(3) c = 3 ''' 1 2 Traceback (most recent call last): File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/代码调试.py", line 18, in <module> demo() File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/代码调试.py", line 16, in demo 1/0 ZeroDivisionError: division by zero 表面在demo()的时候出现了问题,Traceback追踪信息。 '''
- 说明def demo()中有错误,然后慢慢调试。我们发现这种调试比较慢。
-
断点调试,使用pycharm软件,的Debug’代码调试’
- 按F8按钮。
-
出现闪电 标志显示错误地方。
-
使用pdb模块
def func(): print("Hello World") a = 1 b = 2 import pdb pdb.set_trace() func() c = 3
-
参考对应的官方文档:https://docs.python.org/3/library/pdb.html#module-pdb
-
使用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) fib_seq(3000) ''' Traceback (most recent call last): File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 20, in <module> fib_seq(3000) File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 17, in fib_seq res.extend(fib_seq(n-1)) File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 17, in fib_seq res.extend(fib_seq(n-1)) File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 17, in fib_seq res.extend(fib_seq(n-1)) [Previous line repeated 994 more times] File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 16, in fib_seq if n > 0: RecursionError: maximum recursion depth exceeded in comparison 超过python递归深度。 '''
-
python最大递归深度为998,且最大递归深度是可以修改的。
-
cProfile模块
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 import cProfile cProfile.run('fib_seq(30)') ''' 7049218 function calls (96 primitive calls) in 2.765 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 2.764 2.764 <string>:1(<module>) 1 0.000 0.000 2.765 2.765 {built-in method builtins.exec} 31 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 30 0.000 0.000 0.000 0.000 {method 'extend' of 'list' objects} 31/1 0.000 0.000 2.764 2.764 性能分析.py:14(fib_seq) 7049123/31 2.764 0.000 2.764 0.089 性能分析.py:6(fib) '''
- 参数介绍:
- ncalls:函数被调用的次数。如果这一列有两个值,就表示有递归调用,第二个值是原生调用次数,第一个值是总调用次数。
- tottime:函数内部消耗的总时间。(可以帮助优化)
- percall:是tottime除以ncalls,一个函数每次调用平均消耗时间。
- cumtime:之前所有子函数消费时间的累计和。
- filename:lineno(function):被分析函数所在文件名、行号、函数名。
def add(n):
if n == 0:
return 1
else:
return n * add(n-1)
import cProfile
cProfile.run('add(30)')