很多人学Python,代码能跑,但一问到变量怎么存、函数调用时发生了什么,就卡住了。说白了,就是脑子里的内存模型是模糊的。你不画出来,就永远靠猜。
咱们先搭个最简单的场景。你写了个 a = 10。这行代码在内存里干了什么?Python解释器先找一块空闲空间,把数字10放进去,然后在另一个地方登记名字a,让a指向那个存着10的地址。画图的话,就画一个小方框代表内存格子,里面写10,再用一个箭头从a指向它。就这么简单。
再来个复杂点的。列表。b = [1, 2, 3]。这时候内存里有个列表对象,它不是把1,2,3直接塞在一排格子里。列表对象自己占一块内存,里面存了三个引用(就是三个箭头),分别指向存放1、2、3的三个独立内存格子。你画图的时候,要画三个小方框连向三个更小的方框。很多人以为列表是连续的一串格子,错了。Python里几乎一切都是对象引用。
函数调用是重头戏。很多人觉得函数里改了参数,外面变量也跟着变。你画一下栈帧就懂了。每次调用函数,Python会在内存里压入一个新栈帧。栈帧里存着这个函数的局部变量。举个例子:
```python
def add_one(x):
x = x + 1
return x
num = 5
result = add_one(num)
```
num 和 x 一开始指向同一个5。但执行 x = x + 1 时,Python新建了一个6,让x指向6。num还是指向原来的5。画图时,栈帧里画一个x指向6,主程序里画一个num指向5,两个箭头分叉了。这就是传对象引用,不是传引用也不是传值。理解了这点,你就不会写出改了参数影响外部变量这种乌龙。
再来看个坑。默认参数用可变对象,比如列表。
```python
def add_item(item, my_list=[]):
my_list.append(item)
return my_list
```
很多人以为每次调用都是一个新的空列表。画内存图就破案了。函数定义时,Python就创建了一个列表对象,存在函数对象的属性里。每次调用没传my_list,就直接用那个现成的列表。第一次调用加一个元素,第二次调用再加一个,它一直在累积。画出来就是函数对象旁边挂着一个慢慢变长的箭头。你一看就明白,再也不会上当。
变量赋值没你想的那么简单。a = b = [1, 2] 这行,两个变量指向同一个列表。你改a[0],b也变。画图就是两个箭头指向同一个列表方框。单独画两个方框的,是切片的深拷贝。比如 c = b[:],这时候会复制一个列表,b和c是两个独立方框,里面的元素引用可能相同。你得按实际画,按实际记。
垃圾回收也是内存模型的一部分。本地变量不再被引用时,对应内存会标记可回收。画着画着你会发现,函数返回后,栈帧清空,箭头消失,那些没人用的内存格子就变成灰色等待回收。你的图里多画几个空箭头,写个“等待GC”,这印象就深了。
画一遍内存模型,就是把你脑子里的模糊感觉变成确定的图形。刚开始画很慢,画一个简单脚本要十几分钟。画几次后,你一看到代码,脑子里就能自动浮现那些箭头和方框。你调试bug的速度会快很多。遇到异常报错,你也能顺着箭头找到问题在哪。这不是天赋,是练习的结果。
拿起笔来,或者用画图工具。从最简单的赋值开始,一个格子一个箭头地画。画完列表再来字典,画完函数再加闭包。你会发现自己从“猜它怎么运行”变成了“知道它怎么运行”。那种感觉,就跟打通任督二脉一样。