变量与类型 — 可变/不可变这条分界线¶
这一章讲六件小事,但只为一件大事服务:Python 里哪些东西能被原地改掉,哪些不能。 这条分界线(dict 和 list 在一边,str、tuple、int、float 在另一边)决定了你写函数时的契约、 赋值时的陷阱、以及浮点数比较时为什么会踩雷。读完你会拿到一条能复述给别人的判断: 「数据会不会被我看不到的地方改掉?」——能,如果它是可变的。
1. 这一章讲什么¶
原书第 3 章(Components of Code,代码的零件)从这里起步:变量是代码最基本的构件, 然后是字符串、列表、dict、tuple——「Python 里最重要的数据结构,也是任何其他语言里最重要的」。1
本章覆盖它的前半段,主线例子有两条,都贯穿到底:
- 赋值陷阱:
x = [1, 2, 3]; y = x; y.append(4)—— 现在 x 是什么? - 浮点雷区:
1 - 2/3 == 1/3—— 数学上恒真,Python 里返回 False。2
顶层全景:
变量 = 名字 + 值
├─ 赋值 y = x:拷的是「名单条目」不是「名单本身」→ 可变对象两边共享,改一边等于改两边
├─ 类型是动态的:不声明,跟着值走;鸭子类型:不看血统看行为
├─ None:还没有值,本身就是一种值
├─ 数字:整数永远精确,浮点永远近似 → 不要比相等,要比差距
└─ 布尔:True/False 与 and/or/not,以及「空即 False」的隐式转换
图说:左半边(赋值、类型)讲「名字与对象的关系」,右半边(None、数字、布尔)讲「值本身的形状」。
2. 可变与不可变:函数契约的地基¶
2.1 定义与一次对照实验¶
解决什么问题: 调用一个函数之后,你传入的东西还是原来那个吗?
怎么做: 两个对象,一个可变一个不可变,分别传给同一个函数:3
def add_one(x):
# do something with x
pass
my_value = 5
add_one(my_value) # 之后 my_value 一定是 5 —— 整数不可变,函数动不了它
my_list = [5]
add_one(my_list) # 之后 my_list 是什么?你必须读实现的每一行才知道 —— 列表可变
为什么有效: 不可变对象(int、str、tuple、float)创建之后不能被原地修改——想「改」只能造一个新的再绑回去; 可变对象(list、dict)可以。于是调用者的知识产生了不对等:传不可变参数,函数签名就是完整的契约; 传可变参数,契约上多出一行看不见的「此函数可能改动你的数据,请阅读实现」。4
作者补了一个对照:C++ 里可以用 const 关键字声明「这个参数不许改」;Python 没有这层保护,
你只能自己记住每种类型在哪一边——这一条到第 10 章(函数)还要再砍一次。5
判断(我们的,不是书里的): 「不可变让契约完整」是函数式设计偏爱纯函数的理由之一, 本书不说术语,但处处是这个原则。带走的用法:函数默认不改动传入的可变对象; 真要改,让函数名说出来(比如
sort就地排,sorted返回新列表)——第 04 章会看到这个区分。 如果错,会错在: 性能敏感的代码里,原地修改大列表是正当优化;那时契约要靠命名和文档讲明,而不是靠类型。
2.2 赋值的陷阱:主线例子走一遍¶
解决什么问题: y = x 到底是「复制了一份」还是「又指了一下同一份」?
怎么做: 逐行走主线例子:2
走完:x 现在是 [1, 2, 3, 4]——不是你以为的 [1, 2, 3]。
因为 y = x 创建的是新引用(reference,指向对象的名字条目),不是新对象;
两个名字拴在同一份数据上,动哪个都等于动全部。6
怎么修: 想真的要一份独立的列表,显式复制:
为什么有效、边界在哪: 作者明说「这是 Python 常见的错误来源」。
边界:copy() 只复制第一层——列表里套着列表时,内层仍然共享(浅复制);
要全部分开需要 copy.deepcopy()。书里没展开这一层,但「共享引用」的直觉要先立住。
3. 动态类型与鸭子类型:自由的双刃剑¶
解决什么问题: 不写类型声明,代码短了;但读者(和几个月后的你)面对 my_savings 时,
不知道它是数字还是自定义对象。7
怎么做: Python 的类型是动态的:解释器根据你赋的值自动定类型,而且随时能换——
x = 5 之后 x = "Hello",同一个名字换了类型,语言允许。8
作者对此的态度是全书口吻的缩影:「你能这么做,不代表你该这么做。」(only because you can do this doesn’t mean you should) 理由:变量中途换值已经够让读者困惑了,换类型更糟。规矩:一个名字一个用途,不复用变量。 「如果新值需要一个容器,就起个新名字;如果你觉得旧名字起得实在太好——那就给旧变量改名。」9
鸭子类型(duck typing): 一个更有名的俗语——「如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子。」 Python 不关心对象的确切类型,只关心它支不支持你要用的那些操作(接口)。 作者的评价是「非常双刃」:自由的代价是,没有一致的命名约定时,你无法确定一个对象支持什么操作—— 又是「没有编译器检查」的一个侧面。10
全局变量:别碰¶
借变量的话题,书里插了一段对全局变量(在函数体外定义、到处都能读写的变量)的讨伐:
下面的代码能跑,但它「很可怕」——函数 print_name() 依赖什么,你必须通读代码才知道;
把名字当参数传进去,只多花一行,函数需要什么就变得一目了然。11
# 坏:依赖藏在函数体外的全局名字
name = "Donald Duck"
def print_name():
print(name)
# 好:依赖写进了签名
def print_name(name):
print(name)
原话:「全局变量会把你的代码变成一团乱麻。程序员从编程诞生那天起就在试,结论早就出来了——这是个坏主意。」 这是封装(encapsulation,把数据和操作捆在对象里、对外只露接口)的反面,第 11 章会正面讲。
4. None:表示「还没有值」的值¶
解决什么问题: 一个字段(对象或记录里按名字存的数据槽)此刻没有值,该用什么占位?比如一个人有出生日期,但死亡日期尚不可知。12
怎么做: Python 有专门的一个值 None 表示「值的缺席」:
person = {"name": "John",
"birth_date": "1990-01-01",
"death_date": None} # 尚不可知 → 用 None,而不是空字符串或 0
两个细节:作者提醒别用字符串当日期(那串 "1990-01-01" 只是示例,正式该用 datetime 库,见第 14 章);
也别用 None 之外的东西表达「空」——空串和 0 都是合法的值,只有 None 明确表示「没有值」。
边界: 对 None 调用方法会立刻报 AttributeError;检查「是不是 None」要用 is None,不要用 == None
(书里没写这句,补充:这是 Python 的通行规矩,理由与「只该有一个 None」的实现细节有关,记住用法即可)。
5. 数字:整数是你的朋友,浮点是近似¶
5.1 浮点数的根本局限¶
解决什么问题: 为什么 1 - 2/3 算出来是 0.33333333333333337——多出来一个 7?
怎么做: 浮点数(floating point,带小数点的数)用有限个二进制位存数,不可能精确表示所有实数;
Python 的有效数字约 15 位。2/3 就是其中一个无法精确表示的数,于是 1 - 2/3 带着舍入误差落地。13
作者说,关于浮点数只需要记住一件事:
永远不要用 == 比较浮点数。
因为误差是普遍存在的,且隐藏得很深。最毒的一种是随机失败:
数学上 (a + b) + c = a + (b + c) 恒成立,但对浮点数不成立——
而不同版本的 Python、不同平台可能选择不同的加法顺序,于是同一段代码有时对、有时错,
这是「极难发现的 bug」。14
正确的比较姿势是带容差(tolerance):看两个数的差距是否小于某个很小的数:
为什么有效、边界在哪: 容差比较把「数学相等」换成「工程上足够近」,这才是浮点世界的真相等。
真要精确表示十进制小数(比如算钱),用 decimal 模块;作者坦白他自己从没用过——「浮点用对了,大多数场景够了」。
5.2 整数:Python 里最可靠的存在¶
整数没有精度问题:1 + 2 == 3 永远成立,可以安全地用 == 比较。
大多数语言里整数有上限(32 或 64 位),溢出会出恶心 bug;
Python 的整数可以任意大——代价是大整数的运算变慢。15
两个运算符要记牢:
| 写法 | 含义 | 例子 |
|---|---|---|
// |
整除(向下取整) | 5 // 2 → 2 |
% |
取余 | 5 % 2 → 1 |
/ |
普通除法,结果一定是浮点 | 5 / 2 → 2.5 |
注意第三行:别的语言 / 对整数会截断成 2,Python 不会——/ 一出手就掉进浮点世界,失去「永远精确」的保护。
作者的工程建议:能重组算式就别用除法。比如书里把 1/3 + 1/3 == 1 - 1/3(False)
两边同乘 3 化成 1 + 1 == 3 - 1(True),误差就消失了。16
字符串与数字的交界: 读写文件时数字常以字符串的形式进来(如 "42")。
书里唯一的「好办法」:读进来的第一时间转成数字,写出去的最后时刻才转回字符串;
中间所有计算都对着数字。「别碰字符串,除非万不得已;永远用专门的数据结构。」——这句到第 05 章还会回来。17
6. 布尔:两个值,一套代数¶
解决什么问题: 程序要在「条件成立时做 A,否则做 B」之间选路,条件的材料就是布尔值。
怎么做: 布尔只有两个值:True 和 False。三个运算符:
| 运算符 | 作用的参数个数 | 结果 |
|---|---|---|
not |
1 | 取反:not True → False |
and |
2 | 全真才真:True and False → False |
or |
2 | 有真即真:True or False → True |
原书把八种组合(两个输入各取真假,not 两种)逐一列了真值表,值得自己敲一遍。18
布尔代数的另一半是隐式转换:Python 会把 0、空字符串 ""、空列表 [] 当成 False,
其他一切当成 True——bool([]) 是 False,bool([0]) 是 True(注意:[0] 不是空列表,所以为真)。
于是 if x: 实际是「x 非空且非零」的简写。方便,但「布尔用得太多本身就是代码太复杂的信号」,第 08 章展开。19
7. 作者的判断与证据¶
书里给了证据的: 浮点误差示例、加法结合律失效示例、容差比较写法,都可直接运行验证;1314 可变/不可变的函数对照,可直接运行。3
作者的判断(经验断言):
- 「这是常见的错误来源」(赋值共享)——经验之谈,但普遍被行业认同;
- 「能不用下标就不用、能不用浮点就不用」的强语气——是立场,不是定理;
- 「全局变量是坏主意」——本书最强硬的几句之一,也是软件工程四十年的共识。
8. 边界与局限¶
- 浅复制 vs 深复制没展开,只给了
copy();嵌套结构的复制陷阱要自己补。 - NaN、inf 只在第 06 章的异常例子中露了一面。
- 布尔部分没讲短路求值(
A and B在 A 为假时不算 B)——这是重要机制,但原书确实没写,别在这里找。 - 类型部分没讲
isinstance等内省工具——第 10 章的函数重载一段会用到type(x)。
9. 可带走的¶
本章两条主线例子,一行写完:
① y = x; y.append(4) → 可变的 list 被两个名字共享,x 变成 [1, 2, 3, 4];
要独立副本必须 y = x.copy()。2
② 1 - 2/3 == 1/3 → False(浮点近似);
正确姿势是 abs(a - b) < 10**-10(容差比较)。14
- 可变:list、dict;不可变:int、float、str、tuple——这条分界线决定函数能不能暗中改你的数据。
y = x共享同一份对象;要副本,显式.copy()(注意:只有一层)。- 动态类型不是没类型;鸭子类型看行为不看血统;别复用变量,一个名字一个用途。
- 全局变量别碰;函数要什么就写什么进参数。
- None 专管「还没有值」,别拿空串或 0 冒充。
- 浮点是近似:不比相等,比差距(带容差); 能不用除法就不用;要精确十进制用
decimal。 - 整数永远精确、可以任意大,是 Python 里最好用的数字。
- 空即 False:
if x:是「非空非零」的简写,但别滥用——布尔太多是复杂的信号。
10. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 本章定位(代码的零件) | Mutable and Immutable Objects | text/07-fm-mutable-and-immutable-objects.txt:3(搜「basic components」) |
| 可变/不可变定义与清单 | Mutable and Immutable Objects | text/07-fm-mutable-and-immutable-objects.txt:10(搜「changed after」) · text/07-fm-mutable-and-immutable-objects.txt:19(搜「lists and dictionaries」) |
| 函数参数对照实验 | Mutable and Immutable Objects | text/07-fm-mutable-and-immutable-objects.txt:26(搜「add_one」)与 text/08-fm-assignments.txt:2(搜「still 5」) |
| const 对照与 C++ | Assignments | text/08-fm-assignments.txt:23(搜「const」) |
| y = x 共享引用陷阱 | Assignments | text/08-fm-assignments.txt:35(搜「shoot in your foot」) · text/08-fm-assignments.txt:48(搜「x.copy」) |
| 动态类型、不换类型 | Types | text/09-fm-types.txt:11(搜「common misconception」) · text/09-fm-types.txt:25(搜「change the type」) |
| 鸭子类型双刃 | Types | text/09-fm-types.txt:20(搜「duck typing」) |
| 变量两属性、别复用 | Types / None | text/09-fm-types.txt:40(搜「cornerstone」) · text/10-fm-none.txt:5(搜「overwritten」) |
| 全局变量之恶 | None | text/10-fm-none.txt:33(搜「global variable」) · text/10-fm-none.txt:46(搜「spaghetti code」) |
| None 的用途 | None | text/10-fm-none.txt:None(搜「None」) · text/11-fm-numbers.txt:12(搜「death date」) |
| 浮点 15 位、2/3 误差 | Numbers | text/11-fm-numbers.txt:33(搜「finite precision」) · text/11-fm-numbers.txt:39(搜「0.33333333333333337」) |
| 禁止 == 比浮点、加法结合律失效 | Numbers | text/11-fm-numbers.txt:47(搜「Never compare」) · text/11-fm-numbers.txt:63(搜「10**-16」) |
| 容差比较、decimal 模块 | Numbers | text/11-fm-numbers.txt:72(搜「tolerance」) · text/11-fm-numbers.txt:79(搜「decimal module」) |
| 整数任意大、//、% | Numbers / Booleans | text/11-fm-numbers.txt:89(搜「arbitrarily large」) · text/12-fm-booleans.txt:10(搜「floating point number」) |
| 字符串↔数字转换纪律 | Booleans | text/12-fm-booleans.txt:26(搜「as soon as you read」) |
| 布尔三运算符、真值表 | Booleans | text/12-fm-booleans.txt:35(搜「truth values」) · text/13-fm-lists.txt:2(搜「not True」) |
| 隐式转换与 if x | Booleans | text/20-ch04-4-loops-and-if-statements.txt:162(搜「bool(0)」) |
-
出处:「Mutable and Immutable Objects」第 3–6 段(
text/07-fm-mutable-and-immutable-objects.txt:3,搜「basic components」)。 ↩ -
出处:「Assignments」第 37–48 段(
text/08-fm-assignments.txt:35,搜「shoot in your foot」;第 41 段,搜「[1, 2, 3, 4]」)。 ↩↩↩ -
出处:「Mutable and Immutable Objects」第 26–33 段(
text/07-fm-mutable-and-immutable-objects.txt:26,搜「add_one」)。 ↩↩ -
出处:「Mutable and Immutable Objects」第 21–25 段(
text/07-fm-mutable-and-immutable-objects.txt:24,搜「good thing」)。 ↩ -
出处:「Assignments」第 23–25 段(
text/08-fm-assignments.txt:23,搜「const」)。 ↩ -
出处:「Assignments」第 43–44 段(
text/08-fm-assignments.txt:43,搜「new reference」)。 ↩ -
出处:「Types」第 11–19 段(
text/09-fm-types.txt:17,搜「my_savings」)。 ↩ -
出处:「Types」第 11–16 段(
text/09-fm-types.txt:11,搜「common misconception」)。 ↩ -
出处:「Types」第 30–36 段(
text/09-fm-types.txt:30,搜「doesn’t mean you should」)与「None」第 13–17 段(text/10-fm-none.txt:14,搜「confusion」)。 ↩ -
出处:「Types」第 20–24 段(
text/09-fm-types.txt:20,搜「duck typing」)。 ↩ -
出处:「None」第 25–44 段(
text/10-fm-none.txt:33,搜「dreadful」;第 45 段,搜「changed from anywhere」)。 ↩ -
出处:「None」第 5–20 段(
text/11-fm-numbers.txt:12,搜「death date」;「不要用字符串当日期」见第 19 段,搜「datetime library」)。 ↩ -
出处:「Numbers」第 30–43 段(
text/11-fm-numbers.txt:36,搜「15 digits」;第 38 段,搜「0.33333333333333337」)。 ↩↩ -
出处:「Numbers」第 47–75 段(
text/11-fm-numbers.txt:58,搜「(a + b) + c」;第 69 段,搜「extremely hard to find」;第 74 段,搜「tolerance」)。 ↩↩↩ -
出处:「Numbers」第 83–91 段(
text/11-fm-numbers.txt:89,搜「arbitrarily large」)。 ↩ -
出处:「Booleans」第 7–19 段(
text/12-fm-booleans.txt:10,搜「floating point number」;第 16 段,搜「False」)。「两边同乘 3」的重构见text/12-fm-booleans.txt:14–19 段。 ↩ -
出处:「Booleans」第 21–30 段(
text/12-fm-booleans.txt:26,搜「as soon as you read it」)。 ↩ -
出处:「Lists」开头处的真值表(
text/13-fm-lists.txt:2,搜「not True」)。注意该表实际归属 Booleans 一节,是跨文件排版的延续。 ↩ -
出处:「4 Loops and If Statements」第 160–175 段(
text/20-ch04-4-loops-and-if-statements.txt:162,搜「bool(0)」)与text/20-ch04-4-loops-and-if-statements.txt:181(搜「no if statements at all」)。 ↩