02- 数据类型与内存模型

本节目标:把”变量是盒子”的错误心智换成”标签 + 对象”,理解 Python 可变/不可变的灵魂分界,掌握赋值的本质,识别三大经典坑(参数传递、可变默认参数、二维列表),并区分 ==/is 和 浅拷贝/深拷贝。

一、打破”变量是盒子”——标签 + 对象 心智

反直觉演示

a = [1, 2, 3]
b = a
b.append(4)
print(a)            # [1, 2, 3, 4]    ← 没碰 a,a 也变了
print(b)            # [1, 2, 3, 4]

按”盒子”心智推不通。真相必须重建。

真模型:对象在内存里,变量是贴在对象上的”标签”

  • 内存里有”对象”:列表 [1, 2, 3] 是一个对象,有自己的内存位置
  • 变量名是标签(便利贴),贴在某个对象上
  • a = [1, 2, 3] = 创建一个列表对象,贴上 a 标签
  • b = a = 把 b 标签也贴到同一个对象上——没复制
  • b.append(4) = 改了对象本身,a/b 都看到
执行 b = a 后:

      [a]──┐
            ↓
        ┌──────────────┐
        │ [1, 2, 3]    │     ← 同一对象,两张标签
        └──────────────┘
            ↑
      [b]──┘

核心口诀(必背)

赋值不是复制对象,是把另一个名字(标签)绑到同一对象上。

二、对象三件套:类型 / 值 / 身份

Python 里每个对象都有:

属性含义怎么看
类型(type)哪种东西(int / list / str / dict…)type(x)
值(value)具体内容print(x)
身份(identity / id)内存地址(唯一身份证号)id(x)

id() 验证”是不是同一对象”

a = [1, 2, 3]
b = a
c = [1, 2, 3]              # 另一个新列表,内容相同
 
print(id(a) == id(b))      # True   ← a 和 b 同对象
print(id(a) == id(c))      # False  ← c 是另一对象

类比:人和身份证

  • 对象 = 一个人
  • id = 身份证号
  • 变量名 = 这个人的称呼(同一人可有多种叫法)

三、可变(mutable) vs 不可变(immutable)——灵魂分界

阵营典型类型关键特点
不可变int, float, bool, str, tuple, frozenset, None对象一旦创建,内容永远不能改
可变list, dict, set对象内容可以原地修改

”不可变”的真正含义

对象本身一旦创建,内部内容不能改。“修改”会创建一个新对象。

a = 5
b = a
print(id(a) == id(b))      # True   两标签同对象
 
b = b + 1                  # 实际:算出新对象 6,把 b 重新贴上去
print(a)                   # 5      ← a 没变(还贴在 5 上)
print(b)                   # 6
print(id(a) == id(b))      # False  ← b 已经在另一对象上

画面:

开始:
  a, b ──→ [5]

执行 b = b + 1:
  a    ──→ [5]    ← a 留在原对象
  b    ──→ [6]    ← b 被重新贴到新对象

“可变”的真正含义

可变对象允许原地改(id 不变):

a = [1, 2, 3]
b = a
b.append(4)                # 原地改对象本身
 
print(a)                   # [1, 2, 3, 4]   ← a 也变了
print(id(a) == id(b))      # True           ← 还是同一对象

“原地改” vs “重新赋值”

操作可变对象不可变对象
.append() / .update() / x[0]=...方法/索引修改原地改(id 不变)不允许
x = ...x = x + ...重新赋值换贴新对象(id 变)换贴新对象(id 变)

⚠️ 微妙点:b += [4]b = b + [4] 不一样

# A
a = [1, 2, 3]
b = a
b += [4]                   # 等价于 b.extend([4]),原地改
print(a)                   # [1, 2, 3, 4]  ← a 也变
 
# B
a = [1, 2, 3]
b = a
b = b + [4]                # 创建新列表,b 换贴
print(a)                   # [1, 2, 3]     ← a 不变

→ 这个坑只在可变对象上出现。

四、赋值的本质:换贴标签,从不复制

唯一规则

所有赋值 名字 = 表达式 都只做一件事:让左边那个名字指向右边那个对象。没有任何复制。

不管你写的是:

a = 10
a = [1, 2, 3]
a = b
a = some_func()

都是”让 a 指向右边那个对象”。

“变量似乎变了”只有两种来源

现象真实发生
名字换贴到另一对象(a = a + 1)id 变;其它标签不受影响
对象本身被原地改(a.append(...))id 不变;所有指向该对象的标签都看到变化

哪些操作是”原地改”

  • list:append / extend / insert / remove / pop / sort / reverse / a[i]=... / a += ...
  • dict:update / pop / d[k]=...
  • set:add / update / remove / pop

其它(+*sorted(a)list(a)、字符串 replace…)都是返回新对象,不动原对象。

五、三大经典坑(雷区)

坑一:函数参数”看起来像引用传递”

Python 参数传递机制 = 按对象引用传递:你传进去的是”对象的标签”,函数内的参数名和外部变量名指向同一对象。

# 原地改影响外部
def modify(lst):
    lst.append("hi")
 
a = [1, 2]
modify(a)
print(a)        # [1, 2, 'hi']    ← 影响
 
# 重新赋值不影响外部
def replace(lst):
    lst = ["new"]               # 只是给局部名换贴标签
 
b = [1, 2]
replace(b)
print(b)        # [1, 2]          ← 不影响

函数里 xxx.append()xxx[i]=... 等原地操作影响外面;xxx = 新东西 只影响函数内部,外面看不到。

写函数两条原则:

  1. 想返回新结果 → 不原地改输入,用 x + [1]sorted(x)x.copy() 这种生成新对象的操作
  2. 想修改输入 → 文档里明说

坑二:可变默认参数 🔥

def append_user(name, users=[]):       # 默认 users=[]
    users.append(name)
    return users
 
print(append_user("张三"))             # ['张三']
print(append_user("李四"))             # ['张三', '李四']   ← 残留!
print(append_user("王五"))             # ['张三', '李四', '王五']

原因: 默认参数对象只在 def 执行时创建一次,所有调用共享同一个。

def 执行时:
  函数 append_user 挂着默认参数 users = OBJ_A(空列表)

每次没传 users 的调用都用同一个 OBJ_A,所以"上一次"留下的内容会残留。

正确写法(必背):

def append_user(name, users=None):     # 默认 None
    if users is None:
        users = []                     # 函数体内现建
    users.append(name)
    return users

永远不要把可变对象(list / dict / set)作为函数默认参数。要”默认空集合”用 None,函数体内现建。

坑三:二维列表初始化(地质数据处理常踩)

grid = [[0] * 4] * 3                  # ← 看起来很优雅,实际是地雷
grid[0][1] = 99
print(grid)
# [[0, 99, 0, 0], [0, 99, 0, 0], [0, 99, 0, 0]]
#  三行全被改!

原因: [[0]*4] * 3 创建 3 个标签指向同一个内部列表

正确写法:

grid = [[0] * 4 for _ in range(3)]    # 列表推导式,每行独立
grid[0][1] = 99
# [[0, 99, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]  ✅

六、==is

运算符比较什么
==是否相等
is身份是否相同(等价于 id(a) == id(b))
a = [1, 2, 3]
b = [1, 2, 3]
c = a
 
a == b      # True   内容一样
a is b      # False  不同对象
a == c      # True
a is c      # True   同对象

用哪个

场景用什么
一般值比较==
判断是不是 Noneis None ⭐ (PEP 8 推荐)
判断是不是 True/False一般直接 if x:
判断是不是同一对象is

⚠️ 小整数池陷阱

CPython 把 -5 到 256 的整数缓存起来,导致:

a = 100
b = 100
a is b      # True   巧合(都用缓存对象)
 
a = 1000
b = 1000
a is b      # False  ← 1000 不在缓存,不同对象

铁律:比较值用 ==,比较身份用 is。除 None/True/False 这三个单例外,其它一律用 ==

七、浅拷贝 vs 深拷贝

浅拷贝(Shallow Copy)

复制最外层容器,内部嵌套的可变对象仍共享

import copy
 
a = [1, 2, [3, 4]]
b = a.copy()              # 等价于 copy.copy(a) / list(a) / a[:]
 
print(a is b)             # False   外层是新对象
print(a[2] is b[2])       # True    内部 [3,4] 还是共享
 
b[2].append(99)
print(a)                  # [1, 2, [3, 4, 99]]  ← a 内部也被改!

深拷贝(Deep Copy)

递归复制每一层,整棵对象树都是新的。

import copy
 
a = [1, 2, [3, 4]]
b = copy.deepcopy(a)
 
print(a[2] is b[2])       # False   内部也是新对象
 
b[2].append(99)
print(a)                  # [1, 2, [3, 4]]      ← a 完全不受影响

选择

情况用什么
容器只装基本类型(int/str)a.copy() 即可
容器嵌套了可变对象(list/dict)copy.deepcopy(a)
性能敏感且结构简单浅拷贝(deepcopy 慢得多)

list 浅拷贝的几种写法

import copy
a = [1, 2, 3]
 
b1 = a.copy()
b2 = list(a)
b3 = a[:]
b4 = copy.copy(a)
 
# 深拷贝只有一种
b5 = copy.deepcopy(a)

八、本节核心打包(5 句话)

  1. Python 里变量是”标签”,不是”盒子”——所有变量名都贴在对象上。
  2. 每个对象有类型 + 值 + 身份(id)三件套;id(x) 看身份。
  3. 不可变(int/str/tuple…)= 内容不能改,“修改”实际是换贴新对象;可变(list/dict/set)= 能原地改对象本身。
  4. = 永远是”换贴标签”,从不复制;.append() 等才是”原地改对象”——这是函数参数、默认参数、二维 list 三大坑的根源。
  5. 比值用 ==,比身份用 is;只有 None/True/False 适合 is,整数用 is 会被小整数池坑。

九、关键术语速查表

术语一句话定义
对象(Object)内存里的一个实体,有类型/值/身份
标签 / 变量名指向对象的引用,不是容器
id(x)对象的内存地址(身份证号)
type(x)对象的类型
不可变(immutable)对象内容不能改(int/str/tuple/frozenset/None)
可变(mutable)对象内容可原地修改(list/dict/set)
原地操作在对象本身修改,id 不变,影响所有引用
重新赋值让名字指向新对象,id 变,只影响该名字
按对象引用传递Python 的参数传递机制,传的是对象引用
小整数池CPython 缓存 -5~256,导致 is 比小整数有误导
浅拷贝只复制最外层,内部嵌套共享
深拷贝递归复制每一层,完全独立

十、常见陷阱速查表

写法问题修复
def f(x=[]):默认列表跨调用共享def f(x=None): if x is None: x=[]
grid = [[0]*4] * 3三行指向同一内列表[[0]*4 for _ in range(3)]
if x == None:不推荐if x is None:
if a is 1000:小整数池外行为不可靠if a == 1000:
b = a; b.sort() 期望不改 asort 是原地排序b = sorted(a)
copy.copy(嵌套结构) 期望完全隔离浅拷贝不够copy.deepcopy(...)