a = [1, 2, 3] # 把名字 a 绑到列表对象 [1, 2, 3]
b = a # 把名字 b 也绑到**同一个**对象
a = 100 # 把名字 a 剪断,重新绑到整数对象 100
运行时的数据结构
PyDictObject
(哈希表)。PyUnicodeObject
(变量名字符串)PyObject*
(指向真实对象的指针)。/* CPython 3.11 Objects/frameobject.c 简化 */
typedef struct _frame {
PyObject *f_locals; /* dict: 变量名 -> PyObject* */
...
} PyFrameObject;
1.2 一条赋值语句的旅程
a = [1, 2, 3]
BUILD_LIST
,在堆里新建一个 PyListObject
,地址 0x7f8e4c0b9c80
。STORE_NAME a,
在当前栈帧的 locals 哈希表插入一条记录:"a"
→ value: 0x7f8e4c0b9c80
a
,就查这张哈希表,取出 PyObject*
指针即可。栈帧 locals dict ┌---------┐ │ "a" ---┼---┐ └---------┘ │ ▼ [PyListObject 0x7f8e4c0b9c80] +--+--+--+--+--+--+--+--+ |ob_refcnt|ob_type|...|items|
重绑(a = 100
)只是改哈希表的 value 字段,从列表地址改成整数对象地址。
没有复制对象,也没改对象本身,只是改字典里的一条指针。
具体过程:
源码 → 编译器(compile.c)把 a = [1, 2, 3] 翻译成 字节码 LOAD_CONST 0 (1) LOAD_CONST 1 (2) LOAD_CONST 2 (3) BUILD_LIST 3 STORE_NAME 0 (a) <-- 关键指令 字节码 → 解释器(ceval.c)执行到 STORE_NAME 时 PyObject *v = POP(); // 弹出刚建好的 list PyObject *name = GETITEM(names, oparg); // 常量池里拿到字符串 "a" PyObject_SetItem(f->f_locals, name, v); // 把 "a" -> list 指针 写入 locals dict 以后读到 a 时,字节码是 LOAD_NAME 0 (a) 解释器再查同一张哈希表,取出对应 PyObject* 压栈使用。 因此: 编译阶段:变量名只作为符号放进字节码的常量池,不做地址分配。 运行阶段:解释器用这些符号当 key,去当前命名空间(locals、globals、builtins 三张哈希表)里查 value,取到的就是对象指针。 换句话说: “变量名”只是字节码里的一个整数索引,真正绑定动作发生在运行时哈希表的一次 PyDict_SetItem/PyDict_GetItem。 “贴标签 / 重新绑定”在 Python 里就是对命名空间字典做一次插入或覆盖—— 第一次贴标签:dict 里新增一条 key → value; 重新绑定:同一个 key,value 换成新的对象地址,key 本身不变。 所以: 不是改 key 的值(key 始终是那个字符串 "a"), 只是改这条记录里的 value 字段(指针指向另一个对象)。 例如: b=1 a=b 其中 b = 1 在**当前命名空间(locals dict)**里插入/更新一条记录: key: "b" → value: 指向整数对象 1 的指针。 如果 "b" 原来就存在,只是把 value 换成新指针;不存在就新建条目。 a = b 解释器 先从命名空间里读出 "b" 对应的指针, 然后把这条指针再写一条新记录: key: "a" → value: 同一个指向 1 的指针。 命名空间里于是出现两条独立条目: "a" -> PyLongObject(1) "b" -> PyLongObject(1) # 指向同一个 1 对象 所以: 第一次写 b = 1 是“插入或覆盖”; 第二次写 a = b 是“读取 + 再插入一条新键值对”,不会把 "a" 和 "b" 绑成同一条记录。