跳转至

第 3 章 · 数据模型与"一切皆对象"

本章讲 Python 数据模型的底层逻辑。理解了它,你才能解释那些"看起来很玄"的现象:为什么两个相等的对象 is 却不相等、为什么函数里改了 list 外面也变了、为什么 tuple 能当字典的 keylist 不能。


3.1 一切皆对象

在 Java 里,intdouble 是原始类型(不是对象),String/Integer 才是对象。在 Python 里没有原始类型——数字、字符串、函数、类、模块,全部都是对象

x = 42
x.__class__          # <class 'int'>,连数字都有类
(42).__add__(8)      # 50,加法本质是方法调用
def f(): pass
f.__name__           # 'f',函数也是对象,有属性

每个对象有三件事:身份(identity)、类型(type)、值(value)

id(obj)      # 身份,唯一整数(可粗略理解为内存地址)
type(obj)    # 类型

Java 程序员的视角

相当于 Python 里 int 就是 Java 的 Integer——装箱/拆箱这层完全不存在。


3.2 == vs is:和 Java 正好镜像 ⚠️

这是 Java 程序员最危险的认知陷阱。两边含义正好相反

  • Java

    Integer a = 100, b = 100;
    a == b        // 引用比较,常为 false(除非缓存命中)
    a.equals(b)   // 值比较,true
    
  • Python

    a, b = 100, 100
    a == b        # 值比较,true ✅
    a is b        # 身份(同一对象)比较,常为 true(缓存命中)
    

记住镜像关系

Java Python
比较 .equals() ==
比较引用/身份 == is

致命陷阱

在 Python 里用 == 比字符串、数字、列表是正确做法(Java 程序员的直觉正好相反——在 Java 里 == 比字符串是著名的 bug)。Python 里比较值就用 ==

is 的正确用法:只用来判 None(和单例)

if x is None: ...        # ✅ 标准写法
if x is not None: ...    # ✅
if x == None: ...        # ⚠️ 能跑但不符合规范(PEP 8 禁止)

小整数缓存(和 Java 的 Integer 缓存类似)

Python 缓存 -5256 的小整数(Java 是 -128127),这些范围内的 is 可能意外为真——但不要依赖它

a, b = 256, 256
a is b      # True(缓存)
a, b = 257, 257
a is b      # 模块里常为 True(编译期常量),REPL 里才可能 False

Pythonic 写法

永远用 == 比值、用 is 只判 None/单例。把"小整数缓存"当作"知道了就不踩坑"的知识,而不是可以利用的特性。


3.3 变量是"标签"不是"盒子":引用语义

Java 区分值类型(基本类型,复制值)和引用类型(对象,复制引用)。Python 只有引用语义——变量是一个"名字标签",贴在对象上;赋值是"把标签贴到新对象",不是拷贝

a = [1, 2, 3]
b = a           # b 和 a 贴同一个 list(不是拷贝!)
b.append(4)
print(a)        # [1, 2, 3, 4] —— a 也变了
  • Java

    List<Integer> a = new ArrayList<>(...);
    List<Integer> b = a;       // b 和 a 指向同一对象
    b.add(4);                  // a 也变 —— 同样的行为
    
  • Python

    a = [1, 2, 3]
    b = a           # 同样指向同一对象
    b.append(4)     # a 也变
    

行为上对容器是一致的;差异在于 Python 没有值类型——连整数都是对象,但整数不可变(见下),所以 a = 5; b = a; b = 6 不影响 a(因为 b = 6 是给 b 贴新标签,不是修改 5)。

关键直觉

在 Python 里,问"b = a 是值传递还是引用传递?"——答案是既不是。它是"贴标签":ba 指向同一个对象。是否互相影响,取决于那个对象可不可变


3.4 可变 vs 不可变

不可变(immutable) 可变(mutable)
int float str bool tuple frozenset list dict set

不可变对象"修改"时其实是创建新对象

s = "abc"
s = s + "d"     # s 指向新对象 "abcd",原 "abc" 不变

这就是字符串拼接(大量 +)低效的原因——每次都建新对象。要批量拼接用 "".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])
类似 Java 里 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 要不可变

dictset哈希快速查找,所以 key/元素必须 hashable——即生命周期内哈希值不变。不可变对象通常 hashable,可变对象不是

d = {}
d[("x", 1)] = "ok"    # ✅ tuple 可哈希
d[["x", 1]] = "no"    # ❌ TypeError: unhashable type: 'list'
能否做 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)

参考答案

[1, 2, 3, 4] [1, 2, 3, 4] [1, 2, 3, 5]
True True
False False
b = a 同一对象,b.append 影响 ac = 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,对一个"列表的列表"做深拷贝,确保修改副本不影响原矩阵。

参考答案
def deep_copy_matrix(m):
    return [row.copy() for row in m]   # 每行浅拷贝即可,若元素是不可变标量
# 若元素还可能是可变对象,需递归;此处二维数字矩阵到此即可。

上一章:第 2 章 · 函数 | 下一章:第 4 章 · OOP