Python 变量是绑定到对象上的名字标签,不是装对象的盒子。名字绑定模型解释了 is 与 == 之别、可变默认参数之坑、深浅拷贝之别与垃圾回收的运作方式。
名字绑定,不是盒子
a = [1, 2] 做两件事:创建 list 对象,把名字 a 绑上去。b = a 不复制对象,只是让第二个名字指向同一对象:
a = [1, 2]
b = a # 同一对象的两个名字
b.append(3)
a # [1, 2, 3]——经 b 的修改,a 全部可见实现层面:模块/类作用域的名字存在 dict 里,函数局部名字是数组槽位,两者都指向对象。对象本身的结构见 数据模型与内建序列 与 Python 关键字 object。
is vs == 与 id()
==:调__eq__比较值is:比较身份——两侧是否同一对象id():读取对象身份(CPython 中即内存地址)
a = [1, 2]; b = [1, 2]
a == b # True,值相等
a is b # False,两个独立对象is 的正当用途基本只剩 x is None(None 是单例)。小整数缓存(-5 至 256)与字符串驻留会让 is 偶然返回 True——那是解释器优化,不是语言承诺,禁止依赖。
可变与不可变的分界
- 不可变:int、float、complex、str、tuple、frozenset、bytes——没有原地修改 API,一切「修改」都新建对象
- 可变:list、dict、set、bytearray 及绝大多数自定义实例——支持原地操作
混合嵌套的经典雷区——
t = (1, 2, [3])
t[2] += [4] # 抛 TypeError,但 t[2] 已变成 [3, 4]!原因:+= 先对 list 原地修改成功,随后把结果塞回 tuple 的引用槽位才失败。tuple 的不可变只锁引用槽位,不锁引用指向的对象。
可变默认参数之坑
默认值在 def 执行时求值一次,此后所有调用共享同一对象:
def collect(item, bucket=[]): # 这个 list 只在定义时创建一次
bucket.append(item)
return bucket
collect(2) # [1, 2]——不是 [2]def collect(item, bucket=None):
if bucket is None:
bucket = [] # 每次调用新建
return [*bucket, item]浅拷贝与深拷贝
import copy
lst = [[1, 2], [3, 4]]
shallow = copy.copy(lst) # 等价 lst.copy() / list(lst),只复制外层
deep = copy.deepcopy(lst) # 递归复制全部层级,memo 字典处理循环引用
shallow[0].append(99)
lst # [[1, 2, 99], [3, 4]]——浅拷贝内层共享- 浅拷贝外层新、内层仍是原引用——嵌套可变对象立刻穿帮
- 不可变对象无需拷贝:
tuple(t)对 tuple 直接返回原对象
传参 = call by sharing
实参到形参传的是对象引用的副本,规则全在「原地修改 vs 赋值」之别:
def f(xs, n):
xs.append(9) # 原地修改 → 函数外可见
n = 100 # 赋值只是重绑局部名 → 函数外无感
nums, x = [1], 1
f(nums, x) # nums → [1, 9];x 仍是 1既非 C 的 by value 也非 by reference,术语 call by sharing。类属性与实例属性 中同名属性遮蔽与查找顺序,是同一名字模型在类命名空间上的翻版。
del 只删名字
del a 解除名字绑定,不销毁对象;对象在引用计数归零时才回收:
a = [1, 2]
b = a
del a # 名字 a 消失,对象仍被 b 引用
b # [1, 2]引用计数与分代 GC
CPython 内存回收是双层结构:
- 引用计数(主):对象记录引用数,赋值/传参 +1、名字消亡 -1,归零立即回收;缺陷是无法处理循环引用
- 分代 GC(辅):标记-清除专逮只被彼此引用的对象群(自引用 list、对象互指);对象按存活时间分代,越老越少被扫描
sys.getrefcount(obj) 可观察计数(传参本身天然 +1);weakref 提供不增加计数的弱引用。
遇到「灵异现象」先问两问:这个对象可变吗?两个名字指向同一对象吗?——绝大多数答案在这两问里现形。