同一个 = 号,装进去的东西差别很大:列表能改,元组和字符串不能。
小整数 0/1/256 的 id 相同,257/1000 就是两个对象。
浅拷贝只复制外层那张皮。
上一节解决了「打出来是乱码」的问题,这一节解决「装进去是什么」。 类型选错的代价,比编码错更隐蔽——它不报错,只是算错。
一、八种常见类型
Demo 列了八种:int、float、str、bool、NoneType、list、tuple、dict。
每一个都打了 type() 和 id()。
id() 是对象在内存里的编号。
它有两个用处:一是判断两个变量是不是同一个对象,二是理解下面的「缓存」和「拷贝」。
新手最常混的是 tuple 和 list:一个是圆括号、一个是方括号,写错了不报错,只是后面改不动。
None 的类型叫 NoneType,不是 none、也不是空字符串。
判断空值只能用 is None,不能用 == None。
二、能改和不能改
Demo 对三种类型都做了同一件事——把第 0 个元素改成 9:
- 列表:改成功,变成 [9, 2]。
- 元组:TypeError,'tuple' 不支持改元素。
- 字符串:TypeError,'str' 也不支持改元素。
这叫「可变」与「不可变」。列表可变,元组和字符串不可变。
不可变的代价是改不了,好处是能当字典的键、能放心共享。选类型时先问一句:这个东西后面要不要改?
三、小整数缓存:is 不是 ==
Demo 的发现很具体:0、1、256 用两种方式造出来,id 相同;
257、1000 就是两个不同的对象。
原因是 CPython 把 -5 到 256 的小整数预先造好、反复复用。超出这个范围,每次都是新建。
所以 a is b 在某些数上碰巧为真、某些数上为假——这不是语言保证的行为,是实现的巧合。
结论只有一句:比大小用 ==,比「是不是同一个对象」才用 is。
is None 是合法用法,is 300 是定时炸弹。
四、浅拷贝只复制一张皮
Demo 造了一个嵌套列表:a = [inner, inner],两个位置指向同一个 [1, 2]。
然后 b = list(a) 做浅拷贝,c = copy.deepcopy(a) 做深拷贝。
改一下 a[0].append(3),三者立刻分家:
- a 和 b 都变成两个元素都是 [1, 2, 3] 的样子——里层还是同一块。
- c 还是两个元素都是 [1, 2] 的样子——深拷贝把里层也复制了一份。
这就是数据预处理里最常见的隐形 bug:你以为是副本,其实改一个动了俩。
标注数据、特征表、标签列表,凡是嵌套结构,一律 deepcopy。
五、动手 Demo:同一个等号,装进去的东西差别很大
造八种类型的值,打印 type()、repr() 和 id();对列表、元组、字符串各试一次改元素;
用两种方式造 0/1/256/257/1000,比较 id;
最后做一次浅拷贝与深拷贝的对比,并演示交换两个变量。
for name, v in vals:
print(name, type(v).__name__, repr(v), id(v))
try:
v[0] = 9 # 列表能过,元组和字符串抛 TypeError
except TypeError as e:
print(name, 'TypeError', e)
a = int(str(256)); b = int(str(256))
print(a is b) # True:小整数被缓存
b = list(a); c = copy.deepcopy(a)
a[0].append(3)
实跑输出:
--------------------------------------------------------------------
变量赋值:同一个 = 号,装进去的东西差别很大
--------------------------------------------------------------------
类型叫法 type() 值 id()
------------------------------------------------
整数 int 38 140721329612872
浮点 float 3.8 2440927295248
字符串 str 'AI' 2440930753536
布尔 bool True 140721328722512
空值 NoneType None 140721328722576
列表 list [1, 2, 3] 2441186658112
元组 tuple (1, 2, 3) 2440930671040
字典 dict {'k': 1} 2441186262272
能不能改:
类型 结果 说明
----------------------------------------
列表 改成功 [9, 2]
元组 TypeError 'tuple' 不支持改元素
字符串 TypeError 'str' 不支持改元素
小整数缓存(同一个值,id 是否相同):
值 id(a) id(b) 判定
--------------------------------------------------
0 140721329611656 140721329611656 同一个
1 140721329611688 140721329611688 同一个
256 140721329619848 140721329619848 同一个
257 2441182998064 2441183002448 两个对象
1000 2441182994608 2441182998064 两个对象
别拿 is 比数值。比大小永远用 ==,is 比的是「是不是同一块内存」。
改 a[0] 之后 a [ [1, 2, 3], [1, 2, 3]]
b = list(a) 浅拷贝 [ [1, 2, 3], [1, 2, 3]]
c = deepcopy(a) [ [1, 2], [1, 2]]
浅拷贝只复制了外层那张皮,里层的列表还是同一块。
a, b = b, a 之后 x=2 y=1
八种类型的 type() 分别是 int/float/str/bool/NoneType/list/tuple/dict。
None 的类型是 NoneType,不是「空」也不是「null」。
改元素那一组:列表打印 [9, 2],元组和字符串都抛 TypeError,说明文字是「不支持改元素」。
这就是可变与不可变的分界。
小整数缓存一目了然:0、1、256 的 id(a) 与 id(b) 一模一样,
257 和 1000 两位数字完全不同。is 在数值上不可靠。
浅拷贝那组:改完 a[0],a 和 b 都出现三个元素,c 还是两个。嵌套结构一定要深拷贝。
最后 x, y = y, x 一行就交换了,不需要中间变量——右边先算成一个元组,再依次赋给左边。
30 秒实验
- 把
is改成==再跑一遍小整数那组,看结果是不是全 True。 - 造一个三层嵌套的列表,
list()和deepcopy()各复制一次,改最里层看谁跟着变。 - 想一想:为什么元组可以作为字典的键,列表不行?
常见误区
- 「
is和==差不多」——小整数上碰巧一样,257上立刻不同。 - 「复制一份就安全了」——浅拷贝只复制外层,里层还是同一块。
- 「列表用得多,元组没用」——要当字典键、要防止被改,就得用元组。
自测三问(点开对答案)
1. 列表、元组、字符串哪个能改元素? —— 只有列表能改;元组和字符串是不可变类型。 2. 为什么 `a is b` 在 256 上为真、在 257 上为假? —— CPython 缓存了 -5 到 256 的小整数,超出范围每次都新建对象。 3. 浅拷贝和深拷贝的区别是什么? —— 浅拷贝只复制外层,里层仍指向同一块;深拷贝连里层一起复制。数据装进变量了,接下来让它们真正算起来——加减乘除里有四个坑。
- ☐ (本节完,下一节 3.2.4 使用Python运算符:进行加减乘除计算)