第 3 章 · 数据模型与"一切皆对象"¶
本章讲 Python 数据模型的底层逻辑。理解了它,你才能解释那些"看起来很玄"的现象:为什么两个相等的对象 is 却不相等、为什么函数里改了 list 外面也变了、为什么 tuple 能当字典的 key 而 list 不能。
3.1 一切皆对象¶
在 Java 里,int、double 是原始类型(不是对象),String/Integer 才是对象。在 Python 里没有原始类型——数字、字符串、函数、类、模块,全部都是对象。
x = 42
x.__class__ # <class 'int'>,连数字都有类
(42).__add__(8) # 50,加法本质是方法调用
def f(): pass
f.__name__ # 'f',函数也是对象,有属性
每个对象有三件事:身份(identity)、类型(type)、值(value)。
Java 程序员的视角
相当于 Python 里 int 就是 Java 的 Integer——装箱/拆箱这层完全不存在。
3.2 == vs is:和 Java 正好镜像 ⚠️¶
这是 Java 程序员最危险的认知陷阱。两边含义正好相反:
-
Java
-
Python
记住镜像关系:
| Java | Python | |
|---|---|---|
| 比较值 | .equals() |
== |
| 比较引用/身份 | == |
is |
致命陷阱
在 Python 里用 == 比字符串、数字、列表值是正确做法(Java 程序员的直觉正好相反——在 Java 里 == 比字符串是著名的 bug)。Python 里比较值就用 ==。
is 的正确用法:只用来判 None(和单例)¶
小整数缓存(和 Java 的 Integer 缓存类似)¶
Python 缓存 -5 到 256 的小整数(Java 是 -128 到 127),这些范围内的 is 可能意外为真——但不要依赖它:
Pythonic 写法
永远用 == 比值、用 is 只判 None/单例。把"小整数缓存"当作"知道了就不踩坑"的知识,而不是可以利用的特性。
3.3 变量是"标签"不是"盒子":引用语义¶
Java 区分值类型(基本类型,复制值)和引用类型(对象,复制引用)。Python 只有引用语义——变量是一个"名字标签",贴在对象上;赋值是"把标签贴到新对象",不是拷贝。
-
Java
-
Python
行为上对容器是一致的;差异在于 Python 没有值类型——连整数都是对象,但整数不可变(见下),所以 a = 5; b = a; b = 6 不影响 a(因为 b = 6 是给 b 贴新标签,不是修改 5)。
关键直觉
在 Python 里,问"b = a 是值传递还是引用传递?"——答案是既不是。它是"贴标签":b 和 a 指向同一个对象。是否互相影响,取决于那个对象可不可变。
3.4 可变 vs 不可变¶
| 不可变(immutable) | 可变(mutable) |
|---|---|
int float str bool tuple frozenset |
list dict set |
不可变对象"修改"时其实是创建新对象:
这就是字符串拼接(大量 +)低效的原因——每次都建新对象。要批量拼接用 "".join(parts)。
⚠️ Java 程序员的陷阱:tuple 里装可变对象
tuple 本身不可变,指的是它的元素引用不能换,但引用指向的对象仍可被修改:
t = ([1, 2], [3, 4])
t[0] = [9] # ❌ TypeError,tuple 不可变
t[0].append(99) # ✅ 合法!t 变成 ([1, 2, 99], [3, 4])
final List 不能重新赋值,但仍能 add。
3.5 浅拷贝 vs 深拷贝¶
b = a 只是贴标签,要拷贝得显式:
import copy
a = [[1, 2], [3, 4]]
# 浅拷贝:新建外层容器,但元素仍是同一引用
b = a.copy() # 或 list(a)、a[:]
b[0].append(99)
print(a) # [[1, 2, 99], [3, 4]] —— 内层被波及!
# 深拷贝:递归复制所有层级
c = copy.deepcopy(a)
c[0].append(88)
print(a) # 不受影响
| 操作 | 效果 | Java 类比 |
|---|---|---|
b = a |
同一对象 | List b = a; |
a.copy() / list(a) |
浅拷贝(一层) | new ArrayList<>(a) |
copy.deepcopy(a) |
深拷贝(递归) | 需要序列化/手写 |
Pythonic 写法
函数里不要就地修改传入的可变参数(有副作用),优先返回新对象。这和 Java 里"避免修改入参"的良好实践一致。
3.6 hashable:为什么 dict 的 key 要不可变¶
dict 和 set 靠哈希快速查找,所以 key/元素必须 hashable——即生命周期内哈希值不变。不可变对象通常 hashable,可变对象不是。
| 能否做 dict key / set 元素 | |
|---|---|
int str tuple(元素也都可哈希时)frozenset |
✅ |
list dict set |
❌ |
需要"可变的 key"?用 tuple 或自定义类并实现 __hash__(第 4 章)。
本章练习¶
练习 3.1
预测输出,并解释:
a = [1, 2, 3]
b = a
c = a.copy()
b.append(4)
c.append(5)
print(a, b, c)
print(a == b, a is b)
print(a == c, a is c)
参考答案
b = a 同一对象,b.append 影响 a;c = a.copy() 浅拷贝独立。a == b 值相等且 is 同一对象;a == c 值不等(4 vs 5)、is 不同对象。
练习 3.2
下面哪些是 Pythonic / 正确的?说明理由。
(a) if name == None: (b) if items is not None and len(items) > 0: (c) if items: (d) if a == b: 判断两个列表内容是否相同。
参考答案
(a) ❌ 应用 is None(PEP 8)。(b) 啰嗦,应简化为 (c)。(c) ✅ Pythonic(truthiness)。(d) ✅ Python 比较值用 ==,列表内容比较正是 ==。
练习 3.3
写一个函数 deep_copy_matrix(m),不用 copy.deepcopy,对一个"列表的列表"做深拷贝,确保修改副本不影响原矩阵。
参考答案
上一章:第 2 章 · 函数 | 下一章:第 4 章 · OOP。