← 返回知识卡片总目录

DS1号 · 自学教材系列 | 编程

第 4 章 变量与内存

程序要处理数据,数据就得先有个地方「住」。这一章讲编程里最基础、也最容易被人误解的概念——变量。我们不止说「变量是装数据的盒子」,更要讲清:变量到底是什么、它和内存是什么关系、写下 x = 5 那一瞬间,计算机的内存里到底发生了什么。

🎯 本章学习目标

  1. 理解为什么程序需要「变量」——数据必须先存下来才能被处理。
  2. 讲清「变量 = 一块命名的内存」这个本质,而不只是「盒子」的比喻。
  3. 理解赋值语句 x = 5 在内存层面发生的具体步骤。
  4. 理解「变量名」和「变量里存的值」是两回事,以及赋值时数据是「复制」还是「引用」。
  5. 会用 id() 这个「观测窗口」,亲眼看到变量挂着哪块内存、重新赋值时发生了什么。
  6. 初识「引用」语义:数字赋值是复制快照,而列表这类可变对象赋值后是共享同一块内存。

4.1为什么需要变量:数据要先「住下来」

回顾上一章的程序 print("Hello, World!"):它把一段文字直接显示出来,然后这段文字就「用完即弃」了。但真实程序里,数据往往要先存起来,之后反复用。

比如你要算「小明有 5 个苹果,又买了 3 个,现在几个?」如果写成:

print(5 + 3)

确实能算出 8。但问题是:那个「5」和「3」是「一次性」的——算完就没了。如果之后你还想用「小明的苹果数」去算别的东西(比如分给 2 个人每人几个),你就得把 5 重新写一遍。数据越多、步骤越复杂,这种「用完即弃」就越要命。

所以我们需要一个东西,能把数据存起来、起个名字、随时取用。这个东西,就是 变量(variable)。

apples = 5        # 把 5 存进名为 apples 的变量
apples = apples + 3   # 苹果数变成 8
print(apples)     # 输出 8

核心思想 变量解决的是「数据要有地方住、要有名字找」的问题。它让数据从「一次性消耗品」变成「可以反复取用的存粮」。

📖 名词溯源:variable 为什么叫「变量」? 英文 variable 来自拉丁语 variare(变化),本义是「会变的、可变的」。它叫「变量」,恰恰因为它存的值可以变——今天存 5,明天可以改成 8。名字不变,值可变,这正是变量的精髓。

更深一层:数据为什么要「住」进内存

「把数据存起来」这句话,还可以再往下问一层:存到哪?为什么非得有个「地方」?CPU 里真正干活的部件叫寄存器(register),可以把它想象成工匠手边的小工具架——数量极少(常见 CPU 只有几十个),取用飞快,但一放下就会被下一个任务占用。而程序要处理的数据动辄成百上千:一次试验的几百条读数、一份名单、一串中间计算结果,工具架根本摆不下。所以数据必须住进大仓库——内存,CPU 每次要用,再按地址来取。变量,就是程序跟这座仓库打交道时的「把手」:起名字=给货位贴标签,赋值=把货存进货位,用名字=按标签取货。

把数据安顿进变量之后,能拿到三样「直接把数字写进算式」永远拿不到的东西。第一是复用:一次存储,处处引用——「备件总数」存进 total,后面十处计算都用这个名字,不必把数字抄十遍。第二是更新:变量永远反映最新状态,新数据一到,覆盖写进去就行,账目始终是活的。第三是可读:算式里出现的是「库存数 + 入库数」而不是两个光秃秃的数字,别人(以及三个月后的你)一眼能看懂每一步在算什么——在工程语境里,这就是计算过程的「可追溯」。

📐 例题 1 把「一次性算式」改写成变量化脚本

某库房的备件台账要算两笔账:现有备件 5 件,今日入库 3 件,先算当前总数;今日又领用 4 件,再算剩余数。(1) 按「数字直接写进算式」写一版脚本;(2) 改写成变量化脚本并运行;(3) 若入库数改为 10 件,两版脚本各要改几处?

思路 第一版把 5、3、4 直接写进算式;第二版让每个数据住进一个「见名知义」的变量,算式只引用名字。然后比较数据变化时各自的修改成本。

解 两版脚本与实际运行结果:

# 版本一:数字直接写死在算式里
print(5 + 3)        # 当前总数:8
print(5 + 3 - 4)    # 剩余数:4

# 版本二:数据进变量,算式引用名字
spare = 5                    # 现有备件数
incoming = 3                 # 今日入库数
issue = 4                    # 今日领用数
total = spare + incoming     # 当前总数
print(total)                 # 8
remaining = total - issue    # 剩余数
print(remaining)             # 4

两版输出相同:8 和 4。但把入库数改成 10 时差距立刻显形——版本一要把两行算式里的 3 都改成 10,漏改一处,账就错;版本二只需把 incoming = 3 这一行改成 incoming = 10,total 自动变为 15、remaining 自动变为 11,其余一字不动。同一数据被引用的次数越多,「改一处」对「改多处」的优势越悬殊。

答:(1)(2) 见代码,输出均为 8、4;(3) 入库数变化时,版本一要改每一处引用(本例 2 处),版本二只改 1 行。 ⚠ 易错点:别小看「改几处」——真实脚本里同一数据往往出现几十次,靠人眼找齐就是事故的温床。让数据只在一处「出生」(赋值行)、在他处「引用」(用名字),从源头消灭「改漏」。

放到工程现场,这个模式无处不在:传感器每秒读一次温度,读到的值必须先存进变量 temp,才能既拿去判断「是否超温」、又拿去写入记录、还能参与后续统计——一次采集,多处使用。如果每个用途都现场重读一次传感器,不仅浪费,而且三次读数可能各不相同,前后账就对不上了。「先存后用」是数据处理的通用节拍:读入 → 存进变量 → 反复引用。

至此,「为什么要有变量」说透了:数据要有地方住、要有名字找、要改得起、要看得懂。下一节把「住处」解剖开看——变量住的那块内存到底长什么样,名字又是怎么跟它挂上钩的。

4.2变量 = 一块命名的内存

很多教材说「变量是一个盒子」。这个比喻对直觉有帮助,但不够。要打地基,我们得知道盒子到底对应现实里的什么。

回到「计算机软硬件」那门课:计算机里有一块叫 内存(memory,RAM)的地方,它由海量的小格子组成,每个格子能存一个 0 或 1(一个 bit)。这些格子被编号,就像一排储物柜,每个柜子有个门牌号(地址)。

那么「变量」是什么?

底层原理 变量 = 内存里的一块(或几格连起来的)存储空间,加上一个方便人类记忆的名字。当你写 apples = 5 时,计算机在内存里划出一小块地方,把「5」的二进制形式存进去,然后让名字 apples 「指向」这块地方。以后你写 apples,计算机就顺着这个名字找到那块内存,取出里面的值。

变量是一块命名的内存 内存是一排编号的小格子,变量名指向其中一块格子,格子里存着值。 内存 = 一排编号的格子(每个格子存 0 或 1) 地址 0 0101 地址 1 1010 地址 2 0000 0101 地址 3 1100 地址 4 0011 apples 变量名(人记这个) 地址 2 那格,存的是「5」的二进制 变量 apples 就「指向」这里
图 4-1内存是一排编号的格子;变量 apples 指向其中某块格子,格子里存着「5」的二进制。变量名是给人看的,地址才是机器找数据用的。

🧠 一句话记忆 变量 = 内存格子 + 名字标签。名字给你看,地址给机器看,格子里才是真正的值。

把「内存」再往下分一层,还得跟另一座仓库划清界限:内存(RAM)和硬盘是两种不同的存储。内存读写极快——CPU 取一次数据按纳秒计,但断电即失;硬盘慢上百倍——按毫秒计,但断电不丢。变量住的是内存:程序运行时,CPU 先把代码和数据装进内存再加工,程序一结束(或一断电),变量连同里面的数据一起蒸发——就像没点保存就关掉的文档。想长期保存的数据必须另走一条路:写进硬盘上的文件,这是第 11 章的主题。今天先记牢这条边界:变量活在内存里,文件躺在硬盘上;内存是工作台,硬盘是档案柜——工作台上东西再顺手,下班也得归档。

用 id() 亲眼看「名字挂着哪块内存」

上面的说法再顺,也还是推理。好在 Python 提供了一个观测窗口:内建函数 id()。把一个值交给它,它返回这个值所属对象在本次运行中的身份编号;在最常见的 CPython 实现里,这个编号正是对象在内存中的地址。换句话说,id() 能让你「看见」图 4-1 里画的那块格子:两个名字是不是挂着同一块格子,一查便知。

顺带认一认地址的「长相」。内存按字节编号,一根几 GB 的内存条要编到几十亿号,写成十进制又长又难念,所以工程师习惯把地址写成十六进制——每 4 个二进制位恰好并成 1 个十六进制字符,长度立刻缩短一半以上,书写时前面加 0x 标记进制。比如十进制的 2026,写成十六进制就是 0x7EA。你将来在调试工具、报错信息里看到的 0x 开头的一长串,多半都是地址。

📐 例题 2 id() 观测:赋值前后,名字挂的是哪块内存

先预测、再运行核对:y = x 之后,x 和 y 挂的是同一块内存吗?接着 x = 10,x 的指向变了吗?y 会跟着变成 10 吗?

思路 用 id() 给「同一块内存」做体检:两个变量的 id 相等,说明两个名字挂着同一个对象;重新赋值后 id 变了,说明名字搬去挂另一块新内存了。

解 实际运行(注释里 id 的具体数字每次运行都会不同,这很正常——地址由系统分配):

x = 5
print(id(x))            # 一串很大的数字(每次运行都不同)

y = x                   # 让 y 也挂上 x 挂着的那块内存
print(id(y) == id(x))   # True:两个名字、同一块内存

x = 10                  # 重新赋值:x 改挂一块存着 10 的新内存
print(id(x) == id(y))   # False:x 搬走了
print(y)                # 5:y 挂的那块内存没动过

实测三个关系全部成立:赋值后 id 相同(同一对象);重新赋值后 id 改变(换了个对象);y 毫发无损。这一幕正是下一节「细节二」的内存级证据:y = x 复制的是「指向」,之后 x = 10 让 x 去挂新对象,y 仍挂在原对象上。

答:y = x 后两个 id 相等(同一块内存);x = 10 后 x 的 id 改变,y 仍指向原内存、值仍是 5。 ⚠ 易错点:id 的具体数字本身没有意义,不同次运行之间的 id 不可比较;有意义的只是同一次运行内两个 id 是否相等。另外别把 id(x) 记成「x 的值的编号」——它是「x 挂着的那个对象的编号」,x 换了对象,id 就跟着换。

运行几遍例题 2 你会发现:id 打印出的数字每次都不一样,甚至同一段代码在不同电脑上跑出的结果完全不同。这不是 bug——内存地址由系统「现场分配」:程序启动时,系统看哪片内存空着就把哪片给你,下次启动空着的区域不同,地址自然不同。由此得出一条使用纪律:id 只用来做「相等与否」的比较,绝不能把具体数字写死进代码或文档。「地址是 140232852871376」这种话离开那次运行就作废,而「两者 id 相等」在任何一次运行里都成立。

现在可以把三层概念正式拆开——这是本章最重要的一组辨析:变量名是给人看的标签;对象(内存块)是真正存数据的地方;地址(id)是机器找对象用的编号。三者关系是「名字 —挂在→ 地址 —里面装→ 值」。新手很多困惑的根源,是把「名字」当成了「盒子本身」——名字不是盒子,是贴在盒子上的纸条;把纸条撕下来贴到另一个盒子上(重新赋值),原来的盒子纹丝不动。

再补一个幕后设定:Python 内部维护着一张「名字 → 对象」的登记表(术语叫命名空间,namespace)。写 x = 5 就是在表里登记「x → 存 5 的那个对象」;执行 print(x) 就是按名字查表、取出对象。这张表的存在解释了两件事:为什么同一个名字可以反复重新登记(赋值就是改表);为什么两个名字可以登记到同一个对象上(y = x 就是照抄一条登记)。第 9 章讲函数时你会看到,这张表还会按「局部 / 全局」分成几片——现在先记住一句话:查名字,就是查表。

最后再揭一层「幕后的幕后」:这些对象的本体到底住在内存的哪个区?程序运行时,把内存分成两片各司其职的区域。堆是一片自由堆放的大仓库——Python 里你写下的每个整数、每个字符串,本体都存放在堆里,由运行环境按需申请空间、用完自动回收;栈则是层层叠放的托盘架——后进先出,专门记录「程序走到哪了、谁在调用谁」:每进入一层调用,架上就叠起一层新托盘,托盘里放着这层调用的现场信息和它的局部名字登记表;调用返回,整层托盘弹掉,登记表随之清空。一句话分工:栈管流程,堆管数据;栈上的纸条(名字)指向堆里的盒子(对象)。现在不必深究细节,只需建立一个直觉——内存不是一片平坦的大盒子,而是分区管理;第 9 章讲函数时你会亲眼看到:栈的层层托盘,就是函数「跳出去还能跳回来」的全部秘密。

4.3赋值语句:内存里发生了什么

现在,我们放慢镜头,看这一行代码执行时,内存里到底发生了什么:

x = 5

这条语句叫 赋值语句(assignment)。注意 = 在这里不是「相等」,而是「把右边的值,存进左边的名字」。它执行时,内存里发生三步:

  1. 分配空间:计算机在内存里找到一块空闲的格子(或几格),划给这个变量。
  2. 写入值:把右边「5」转换成二进制 0000 0101,写进这块格子里。
  3. 绑定名字:把变量名 x 和这块内存地址「绑定」起来,建立一张「名字 → 地址」的对照表。
赋值的三步 赋值时先分配内存,再写入值,最后把名字和地址绑定。 ① 分配空间 找到空闲内存格 划给这个变量 ② 写入值 把 5 转成二进制 0000 0101 ③ 绑定名字 x → 该地址 建立名字对照表
图 4-2x = 5 执行时,内存里依次发生「分配空间 → 写入值 → 绑定名字」三步。

两个关键细节:重新赋值与「复制」

细节一:重新赋值是「覆盖」,不是「塞第二个」。如果接着写 x = 10,不是把 10 塞进旁边新格子,而是把原来的格子里的 5 擦掉、写成 10。名字还是那个 x,指向的还是那块地址,只是里面的值变了。

细节二:「=」右边的值先算出来,再存进去。看这两行:

x = 5
y = x        # y 变成 5,是把 x 当前的值「复制一份」给 y
x = 10       # 之后 x 变成 10,但 y 仍是 5

执行到 y = x 时,计算机先读出 x 的值(5),再把这个 5 复制一份存进 y。之后 x = 10 改的是 x 自己的格子,y 的格子不受影响。这是新手最容易搞错的地方,务必记住:对于数字这类简单数据,赋值是「复制值」,不是「建立联动」。

核心思想 = 是「赋值」不是「相等」。它的完整含义是:先计算右边表达式的值,再把这个值存进左边变量指向的内存格子里。重新赋值就是覆盖旧值。

「先算右边」为什么是铁律

细节二说「= 右边的值先算出来,再存进去」。这个顺序值得追问一句:为什么非这样不可?看这行最典型的代码:apples = apples + 3。它的右侧引用了左侧的变量自己——计算机必须先按名字查到旧值 5,算出 8,然后才覆盖写回。假如反过来「先存再算」,赋值的第一步就把旧值 5 冲掉了,右边再想取 apples 时已经面目全非。所以顺序只能是:读旧值 → 算表达式 → 覆盖写回。

这类「自己更新自己」的写法,含义是「在旧值的基础上更新」,工程上俯拾皆是:库存数 = 库存数 + 入库数;合格数 = 合格数 + 1;总里程 = 总里程 + 本段里程。注意一个耐人寻味的反差:数学里 x = x + 3 是无解方程(哪个数会比自己大 3?),编程里它却是出现频率最高的语句之一。这个反差恰恰是提醒——= 是动作(存入),不是关系(相等)。把数学直觉原样搬进赋值,就会在这里摔跟头。

📐 例题 3 三步赋值全追踪:a 和 b 最后各是多少

逐行执行 a = 3、b = a、a = 7。(1) 不运行,先写出每步之后 a、b 的值;(2) 写一段脚本,在每步之后打印 a、b 验证预测;(3) 用一句话解释为什么最后 a、b 不一样。

思路 给每步赋值拍一张「现场快照」,口诀是:「= 右边先算,算完存给左边;存的是值的快照,不是连线」。

解 验证脚本与实际输出:

a = 3
print("第1步后:", a)      # 第1步后: 3
b = a
print("第2步后:", a, b)   # 第2步后: 3 3
a = 7
print("第3步后:", a, b)   # 第3步后: 7 3

预测与实测一致:第 2 步 b = a 时,把 a 当时的值 3 复制成快照交给 b,此刻 a、b 恰好都是 3;第 3 步 a = 7 只是让 a 改挂存 7 的新内存,b 手里的快照不受影响。一句话解释:b = a 存的是「那一刻的值」,不是「跟着 a 变的联动关系」。

答:a = 7,b = 3。 ⚠ 易错点:不少新手认为 b = a 建立了「同步关系」,于是推断 b 会跟着变成 7。请记住快照模型:赋值完成的那一刻,「复制」就结束了,此后各改各的。(例题 4 马上会给你看一个「反例」——有一种情况,改动真的会被对方看见。)

用工程现场的语言再翻译一遍:变量是一块写着「当前状态」的白板。库存白板每天更新(覆盖写),看到的永远是最新数;如果哪天需要留档「库存的每日变化」,就把历史值抄到别处——另一个变量,或第 11 章要讲的文件。白板养「现在」,档案养「过去」:覆盖写不保留历史,需要追溯的数据必须另行留痕,这是工程数据管理的基本意识。

📐 例题 4 引用语义初现:数字是快照,列表是共享

先预测、再运行验证:(1) n = 5、m = n、n = n + 1 之后,m 是多少?(2) a = [1, 2, 3]、b = a、a.append(4) 之后,b 是多少?([ ] 是「列表」——一次能装多个值的容器,第 10 章正式讲;append 的意思是「往里添加一项」。)

思路 关键区别在「修改方式」:n = n + 1 是让 n 改挂一块新内存(结果 6 存在新地方);a.append(4) 是在 a 挂着的那块内存里原地扩内容,a 并没有搬家。如果 b 与 a 共享同一块内存,内容原地变了,b 看到的自然也变了。

解 验证代码与实测输出:

n = 5
m = n               # m 与 n 挂同一块内存(存 5 的那块)
n = n + 1           # 算出 6 → 存进新内存 → n 改挂新内存
print(m)            # 5:n 搬家了,m 还挂着原来的内存

a = [1, 2, 3]
b = a               # b 与 a 挂同一个列表对象
a.append(4)         # 原地往这块内存里添加 4,a 没有搬家
print(b)            # [1, 2, 3, 4]:同一块内存的内容变了,b 看到的也变了
print(id(a) == id(b))   # True:从头到尾都是同一块内存

实测:(1) m = 5,纹丝不动;(2) b 变成了 [1, 2, 3, 4],且两个 id 相等。两幕对比,差别全在「后来发生了什么」:数字的重新赋值=名字搬家(改挂新对象,旧对象无人动);列表的 append=原地扩建(名字没动,对象内容变了)。两种情况里 b = a 复制的都是「指向」,但从那一刻起,走出了两条完全不同的路。

答:(1) m = 5;(2) b = [1, 2, 3, 4]。 ⚠ 易错点:别把结论背成「数字复制、列表不复制」——两种情况复制的都是「指向关系」,真正的分歧在后续操作:重新赋值让名字搬家(互不影响),原地修改让内容变化(共享者都看得见)。这类「可变对象」是新手 bug 的头号高发区,第 10 章会专门展开;今天先记住现象和原因。

热身小魔术:交换两个变量的值

快照模型还能解释一个经典小戏法——交换两个变量的值。最朴素的做法是找「第三只杯子」当 中转站:先把 a 的值倒进 temp 保住,再把 b 倒进 a,最后把 temp 倒回 b。三步顺序错一步,某个值就会被覆盖丢失——因为覆盖写是「毁旧存新」:

a = 1
b = 2
temp = a      # 先把 a 的值存进「第三只杯子」
a = b         # b 的值倒进 a
b = temp      # 中转站里的旧 a 值倒进 b
print(a, b)   # 2 1:交换完成

而 Python 提供了一步到位的写法:a, b = b, a。这行代码正是「先算右边」铁律的漂亮应用——右边先把 b、a 当前的值按顺序打包成一份新值清单,然后才解包分给左边的两个名字;因为是先打包后分发,任何旧值都不会中途丢失。(打包—解包机制的完整原理,第 10 章讲列表时会正式揭开,今天先用起来。)

a = 1
b = 2
a, b = b, a   # 右边先打包成 (2, 1),再解包给左边的 a、b
print(a, b)   # 2 1

对照着看很有意思:三行版要小心翼翼安排顺序,一行版把「顺序问题」交给了语言机制本身。这也是好语言的样子——把容易出错的细节收编成一句直观的语法,让人腾出脑力去想真正重要的事。往后你会无数次用到 a, b = b, a 这个惯用法,现在就把它的原理记牢。

4.4变量名:名字的规则与意义

变量名是你给内存格子贴的标签。名字起得好,代码读起来像文章;起得烂,三天后你自己都看不懂。Python 对变量名有几条硬规则:

Python 变量名的规则
规则正确示例错误示例
只能由字母、数字、下划线组成age、user_nameuser-name、3x
不能以数字开头score22score
区分大小写age 和 Age 是两个变量—
不能用关键字—if、for、print

除了「合法」,更要「有意义」。给变量起名,要让它自己会说话:

a = 5              # 不好:a 是什么?过几天就忘
apple_count = 5     # 好:一看就知道是「苹果的数量」
total_price = 9.9   # 好:总价
is_student = True   # 好:是不是学生

🧠 一句话记忆 变量名要「见名知义」:让它自己说出它存的是什么,而不是让别人去猜。

关键字为什么碰不得

表格里「不能用关键字」这条规则,值得追问一句为什么。if、for、and、class 这些词是 Python 语法的「骨架」——解释器一读到它们,就走专门的流程:if 开始一次判断,for 开始一轮循环。如果允许把它们当变量名,解释器读到 for = 5 就彻底懵了:这是循环开始,还是取一个叫 for 的变量的值?整套语法都会瘫痪。所以关键字被语言绝对保留,永远不能挪作他用。

这里有一个值得较真的细节:表格把 print 也列进了「错误示例」,但严格来说 print 不是关键字,而是「内建功能名」——它是 Python 出厂自带的打印功能的名字,不走特殊语法流程。机制上你甚至可以写 print = 5(语言允许),但这行会把打印功能的名字占用掉,之后 print(...) 就失灵了——「合法,但致命」。所以「别用 if」和「别用 print」结论相同、性质不同:一个是绝对禁止(关键字),一个是强烈不建议(内建名)。用两行代码可以实测两者的身份差异:

import keyword                     # 引入 Python 自带的「关键字名单」模块
print(keyword.iskeyword('for'))     # True :for 是关键字,绝对不能用
print(keyword.iskeyword('print'))   # False:print 是内建名,能占但千万别占

📐 例题 5 变量名裁判:8 个候选名字逐个过堂

判断以下 8 个名字哪些能当变量名、哪些不能,并说明理由:user_name、_hidden、2score、user-name、class、Class、print、for。

思路 按两道关审查:第一道「构词关」——是否只由字母、数字、下划线组成且不以数字开头(这道关解释器直接管);第二道「身份关」——是否是关键字(用 keyword.iskeyword 查询)。两关之外还有第三道「阅读关」——人看着会不会误解。

解 先跑裁判程序:

import keyword
names = ["user_name", "_hidden", "2score", "user-name",
         "class", "Class", "print", "for"]
for name in names:
    print(name, "是关键字:", keyword.iskeyword(name))

运行输出:user_name False、_hidden False、2score False、user-name False、class True、Class False、print False、for True。逐个裁定——user_name:合法,全小写下划线风格;_hidden:合法,下划线可以打头;2score:不合法,数字开头,过不了构词关;user-name:不合法,连字符会被当成减号,user-name 变成了算式;class、for:构词合法但撞了关键字,绝对禁止;Class:合法(区分大小写,它和 class 是两个名字),但只差一个字母大小写、极易误读,强烈不推荐;print:合法但占用内建功能名,同样强烈不推荐。

答:user_name ✓、_hidden ✓、2score ✗(数字开头)、user-name ✗(连字符当减号)、class ✗(关键字)、Class 合法但不推荐、print 合法但别占、for ✗(关键字)。 ⚠ 易错点:「合法」与「合格」是两回事——Class 和 print 都能让解释器闭嘴,但都会给读代码的人埋雷。命名的完整标准是三关全过:解释器的构词关、关键字的身份关、人的阅读关。

名字登记表:区分大小写的条目 登记表里 age 与 Age 是两个不同条目;age 已登记指向 35,Age 与 AGE 均未登记。 「名字 → 对象」登记表(示意) age → 35(已登记) Age → 未登记(空条目) AGE → 未登记(空条目) 大小写不同 =条目不同 查表按「逐字符」精确比对:差一个字母的大小写,就查不到对方
图 4-3名字登记表按「逐字符」存条目:age 与 Age 是两个不同的条目——这就是变量名区分大小写的底层原因。

规则之上还有「风格」。同一个合法的名字,Python 社区有统一的书写习惯:变量名全小写,单词之间用下划线连接(如 user_name、max_retry、total_price)——这套写法叫 snake_case(蛇形命名),名字里的下划线像蛇在地上爬出一节一节的印子,它是 Python 官方风格指南 PEP 8 的明确建议。有些语言流行 userName 这种「驼峰式」,在 Python 里虽然完全合法,却会让代码在社区里显得格格不入。风格为什么重要?因为代码首先是写给人看的:统一的风格让任何一位 Python 工程师打开你的代码都能零成本上手——在多人协作的工程里,这就是实打实的生产力。

再往工程现场走一步,看两条行业通行的命名习惯。其一,让量纲进名字:长度就叫 length_mm,压强就叫 pressure_kpa——读代码的人不必翻文档就知道单位。工程计算里最危险的事故之一是「无单位数字」:同一根管的长度,一份文档写 3.2(米),另一份写 3200(毫米),混在一起算必错;把单位写进名字,等于让每个数字随身携带身份说明。其二,不该变的量用全大写:如 MAX_LOAD_KPA = 8.0——Python 其实没有「常量」这个语法(你仍然能覆盖它),全大写只是行业约定,提醒读代码的人「别动它」。这两条不是 Python 的规则,是工程行业的规则,今天养成,终身受益。

4.5变量与「类型」的初识

还记得 4.2 说的「格子」吗?一个关键问题是:不同的数据,占的格子数量不一样,存的方式也不一样。

整数 5,存成二进制 0000 0101 就行;但一段文字 "Hello",存的是每个字符对应的编码,占的格子更多、方式也完全不同。计算机必须知道这块内存里存的是「什么种类」的数据,才能正确地读它、用它。这个「种类」,就是 数据类型(data type),简称类型。

Python 会自动判断类型:

age = 5            # 整数类型(int)
price = 9.9        # 小数类型(float)
name = "小明"       # 字符串类型(str)
flag = True        # 布尔类型(bool)

为什么类型这么重要?因为同样一个二进制串,按不同类型去解读,结果完全不同。就像一串数字「0101」,它到底是数字 5,还是别的什么东西,取决于你「约定」它是什么。类型就是这个约定。

同一段字节,两种读法

「类型决定解读方式」这句话可以亲眼看到。拿一个字节 0100 0001 来说:如果它属于一个整数变量,就按位权读,128+1=65;如果它属于一个字符串变量,就按编码表读,查 ASCII 表(下一章的主角)——编号 65 对应字符 'A'。字节一个不差,读出的东西却完全不同。你甚至可以让 Python 亲手演示这两个方向:

print(int("01000001", 2))   # 65:按二进制位权读这串 0/1
print(chr(65))              # A:按 ASCII 表把编号 65 翻译成字符

第一行把 0/1 串当「数」读,第二行把同一串当「文」读。所以前面说「类型是约定、是说明书」,现在可以说得更透:说明书不改字节,只改读法。内存里从来没有凭空多出一个「A」,只是解释的方式换了。

同一段字节,按不同类型解读 同一个字节 0100 0001,按整数解读得 65,按字符解读得字母 A。 同一段字节,两种读法 0100 0001 类型:int(整数) 按位权累加:128+1 65 类型:str(字符串) 查 ASCII 表:编号 65 'A' 字节纹丝未动——类型改变的只是「读法」
图 4-4类型决定解读方式:同一串 0/1,挂上不同类型的变量,读出完全不同的值。

再打一个比方收束这一节。想象一张 Excel 表:同一列数字「36866」,在「编号」列里它是个编号,在「字符串」列里它是五个字符的文字,导出成文本文件后还可能是若干字节——列头,就是这张表的「类型」。数据的「身份」从来不取决于它长什么样,而取决于它所在的列。变量也一样:值到底按什么读,看的是变量登记的类型,不是字节本身的样子。

核心思想 类型告诉计算机「这块内存里的 0/1 该怎么解读」。同样的 bit,按整数读、按文字读、按小数读,结果天差地别。类型,就是解读内存的「说明书」。

还有一件新手常惊讶的事:Python 里同一个名字可以先后挂不同类型的对象——x = 5 之后紧接着 x = "hello",程序完全合法。类型不在名字身上,在对象身上;名字只是登记表的条目,想让条目改指一个字符串对象,随时可以。这种风格叫动态类型。它写起来爽快,代价是「看名字猜类型」不再可靠,所以工程社区约定:要么让名字自带类型暗示(user_name、count),要么临时验证(用 type()、isinstance() 这类工具)。另外值得对照的是:不少老牌语言(如 C)把类型绑在名字上——声明「x 是整数」之后 x 终身只能挂整数,这叫静态类型,好处是编译器提前替你把关。动态与静态各有立场,但两者的底层事实一致:值总是带着类型的,只是「谁负责检查类型」不同。

顺带一提:现代 Python 还允许在赋值时给名字加类型提示,写成 x: int = 5。这叫「类型标注」——注意它不改变动态类型的本质:就算标了 int,之后照样可以 x = "hello",解释器一概不管。它的价值是写给读代码的人看的路标,让团队伙伴不必追进每一处代码就知道「这里应该放什么类型」。工程代码里类型标注越来越常见,将来读到不必惊讶,今天的你只要看懂「: int」是个提示、不改变任何行为就够了。

📐 例题 6 类型体检:用 type() 看清每个值的「标签」

先预测、再运行核对:(1) type(5)、type("5")、type(2.5)、type(True) 各返回什么?(2) 5 * 2 和 "5" * 2 各得什么?(3) 综合两问,说明「类型决定行为」。

思路 type() 会返回一个值所属的类型。同一个「5」,带不带引号身份完全不同;再用乘法检验它们各自能干什么。

解 实际运行:

print(type(5))       # <class 'int'>:整数
print(type("5"))     # <class 'str'>:字符串
print(type(2.5))     # <class 'float'>:浮点数
print(type(True))    # <class 'bool'>:布尔值

print(5 * 2)         # 10:整数乘法,算的是「五的二倍」
print("5" * 2)       # 55:字符串重复,得到两份并排的文字

两个「5」看似一样,行为天差地别:整数 5 参与算术,字符串 "5" 参与重复。输出里的 class 一词现在可以粗读成「类别」——类型的正式叫法,后面章节还会遇到。

答:(1) int、str、float、bool;(2) 10 与 '55';(3) 同一个运算符 * 作用在不同类型上,行为完全不同。 ⚠ 易错点:看到值先看类型——数据「长得像」不代表「身份相同」,"5" 和 5 在内存里是两种完全不同的对象,硬把它们混算,Python 会当场报错(下一章看报错长什么样)。

下一章,我们会把每种类型「是什么、怎么在内存里存、会出什么坑」讲透。这一章你只要记住:变量有类型,类型决定内存怎么解读。

本章小结

  1. 变量解决「数据要有地方住、有名字找」的问题,让数据可反复使用。
  2. 变量 = 一块命名的内存:名字给人看,地址给机器找,格子里存真正的值。
  3. 赋值 x = 5 分三步:分配空间 → 写入值 → 绑定名字;= 是「赋值」不是「相等」。
  4. 重新赋值是覆盖旧值;数字这类数据赋值时是复制值,不是联动。
  5. 变量名要见名知义;变量有类型,类型决定内存里的 0/1 怎么解读。
  6. id() 是观测窗口:赋值后两个 id 相等说明挂同一块内存;重新赋值后 id 改变说明「名字搬家」,另一个名字不受影响。
  7. 赋值复制的都是「指向」:数字重新赋值=搬家(互不影响),列表原地修改=内容变化(共享者都看得见)——「快照」与「共享」的分岔口。
  8. 类型不在名字上,在值上:Python 允许 x 先挂整数再挂字符串(动态类型),所以看数据先看类型。
  9. 「先算右边」是铁律:apples = apples + 3 这类「自我更新」全靠它支撑;交换两个变量用 a, b = b, a,一步到位不失值。

读完本章,你应该能……

思考题

1. 为什么说「变量是盒子」这个比喻不够准确?更准确的说法是什么?

提示:比喻只说对「装东西」,没说清「装在哪」。更准确是:变量是内存里一块被命名、可寻址的存储空间。

2. 写出 a = 3; b = a; a = 7 三步执行完后,a 和 b 各是多少?

提示:a=7,b=3。因为 b=a 是把 a 当时的值 3 复制给 b,之后改 a 不影响 b。

3. = 在编程里是「相等」吗?它真正的意思是什么?

提示:不是相等,是赋值:先算右边表达式的值,再存进左边变量。

4. 为什么「类型」很重要?同一个二进制串按不同类型读会怎样?

提示:类型决定内存里的 0/1 怎么解读;同样的 bit,按整数、文字、小数读结果完全不同。

5. 执行 x = 5、y = x 后,id(x) == id(y) 是 True 还是 False?接着 x = 10 之后呢?y 的值会变吗?

赋值后为 True——两个名字挂着同一块内存;x = 10 后为 False——x 搬去挂存 10 的新对象,y 仍挂在原内存上,值仍是 5。id 相等与否,是判断「是否同一对象」最直接的观测手段。

6. a = [1, 2, 3]、b = a、a.append(4) 之后,b 是多少?为什么这里 b 会「跟着变」,而例题 3 里 b = a 之后 a 变了 b 却不变?

b 变成 [1, 2, 3, 4]。因为 append 是「原地修改」:a 没有搬家,还是那块内存,内容变了,共享同一块内存的 b 自然看得见;例题 3 里 a = 7 是「重新赋值」,a 搬去挂新对象,b 还在原对象上,所以不变。口诀:搬家互不影响,原地改动大家共见。

7. if 和 print 都被劝退不能用,两者的「性质」一样吗?

不一样。if 是关键字,被语言绝对保留,用 keyword.iskeyword('if') 查得 True,语法层面直接报错;print 只是内建功能名,查得 False,机制上可以占用但会弄坏打印功能,属于「合法但致命」。判别工具就是 keyword.iskeyword()。

8. 不引入第三个变量,怎样交换 a 和 b 的值?这行代码为什么不会把旧值弄丢?

一行搞定:a, b = b, a。按「先算右边」铁律,Python 先把右边 b、a 的当前值打包成一份新值清单,再解包分给左边的 a、b——先打包后分发,所以任何旧值都不会在中途被覆盖。若没有这个机制,就得借「第三只杯子」:temp = a; a = b; b = temp,而且三步顺序不能乱。

9. 「数据本体的存放区」和「调用流程的记录区」分别对应内存的哪一片?各自的存取特点是什么?

提示:对象本体在堆——自由堆放、按地址取用、由运行环境申请回收;调用记录在栈——层层叠放、后进先出、返回即弹。栈上的名字(纸条)指向堆里的对象(盒子)。

10. 函数执行完返回后,它内部的局部名字为什么会「自动消失」?用托盘架解释。

提示:函数的局部名字登记表就放在栈上属于它那层的「托盘」里;函数一返回,整层托盘后进先出被弹掉,上面的登记随之清空——不是有人挨个擦掉变量,而是整层现场一起退场。第 9 章会正面展开这套机制。