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 提供不增加计数的弱引用。

遇到「灵异现象」先问两问:这个对象可变吗?两个名字指向同一对象吗?——绝大多数答案在这两问里现形。