🧠 第三章 编程语言
📄 一句话概括

同一个 = 号,装进去的东西差别很大:列表能改,元组和字符串不能。 小整数 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 秒实验

  1. 把 is 改成 == 再跑一遍小整数那组,看结果是不是全 True。
  2. 造一个三层嵌套的列表,list() 和 deepcopy() 各复制一次,改最里层看谁跟着变。
  3. 想一想:为什么元组可以作为字典的键,列表不行?

常见误区

  1. 「is 和 == 差不多」——小整数上碰巧一样,257 上立刻不同。
  2. 「复制一份就安全了」——浅拷贝只复制外层,里层还是同一块。
  3. 「列表用得多,元组没用」——要当字典键、要防止被改,就得用元组。
自测三问(点开对答案) 1. 列表、元组、字符串哪个能改元素? —— 只有列表能改;元组和字符串是不可变类型。 2. 为什么 `a is b` 在 256 上为真、在 257 上为假? —— CPython 缓存了 -5 到 256 的小整数,超出范围每次都新建对象。 3. 浅拷贝和深拷贝的区别是什么? —— 浅拷贝只复制外层,里层仍指向同一块;深拷贝连里层一起复制。

数据装进变量了,接下来让它们真正算起来——加减乘除里有四个坑。

相关笔记