第 03 章 · 变量、对象与数据类型
这一章要解决什么:把一支电芯的参数存进变量并打印出来。 听起来简单,但这里面藏着三个后面会反复出现的基础认知:变量是"标签"、类型决定能做什么、类型转换。
本章你需要完成的代码:demos/ch03-types.py
3.1 变量:给值起个名字
cap_rated = 2.5 # 额定容量(Ah)
voltage = 3.7 # 标称电压(V)
temp = 25 # 温度(℃)
cell_id = "C001" # 电芯编号(字符串,必须带引号)
is_new = True # 是否新电芯(布尔)逐行看:
| 写法 | 含义 |
|---|---|
cap_rated | 变量名(左边) |
= | 赋值,不是数学里的"等于" |
2.5 | 值(右边),是浮点数 |
# 后面 | 注释,Python 完全忽略 |
读法等号:不是"cap_rated 等于 2.5",而是"把 2.5 这个对象贴上 cap_rated 这个标签"。 这个区别在第 06 章会变成一个真实的坑。
3.2 变量是标签,不是盒子
这是 Python 第一个反直觉的地方,也是理解后面所有"为什么改了 b 结果 a 也变了"的关键。
点开:为什么说变量是"标签"而不是"盒子"?
C 语言的模型(盒子):变量是一个盒子,赋值是把值复制进盒子。
a = [1, 2] → 盒子里装了一个新列表 [1, 2]
b = a → 另开一个盒子,把 [1, 2] 复制进去
b.append(3) → 只改 b 的盒子 → a 还是 [1, 2]Python 的模型(标签):变量只是贴在对象上的一个标签。赋值是把标签贴到已有的对象上。
a = [1, 2] → 创建列表对象 [1, 2],把标签 a 贴上去
b = a → 把标签 b 贴到【同一个】列表对象上(没有复制!)
b.append(3) → 通过 b 修改了那个对象 → a 看到的东西也变了验证:
a = 2.5
b = a
print(a is b) # True —— is 判断"是不是同一个对象"为什么 Python 这么设计:复制大对象(比如一个 10 万行的 DataFrame)代价很高。 默认"共享",需要复制时用 .copy() 明确说出来 —— 这是"显式优于隐式"的体现。
这条认知的直接后果(第 06 章会演示):
a = [1, 2, 3]
b = a # 不是复制!
b.append(4)
print(a) # [1, 2, 3, 4] —— a 也变了3.3 类型:决定这个值能做什么
print(type(2.5)) # <class 'float'> —— 浮点数
print(type(25)) # <class 'int'> —— 整数
print(type("C001")) # <class 'str'> —— 字符串
print(type(True)) # <class 'bool'> —— 布尔值为什么类型重要:它决定了这个值能做什么运算。
2.5 + 1 # ✅ 3.5
"C001" + "A" # ✅ 'C001A' —— 字符串的 + 是"拼接"
2.5 + "1" # ❌ TypeError —— 数字不能和字符串相加最后一行就是零基础最常遇到的报错:
TypeError: unsupported operand type(s) for +: 'float' and 'str'读法:unsupported operand type(s) for + —— "+ 不支持这两种类型"。
四种基本类型
| 类型 | 写法 | 例子 | 用在哪 |
|---|---|---|---|
int | 整数 | 25、-3、0 | 循环次数、计数 |
float | 小数 | 2.5、3.7、1e-3 | 容量、电压、测量值 |
str | 字符串 | "C001"、'A' | 编号、批次、路径 |
bool | 布尔 | True、False | 判断结果、标记 |
点开:还有哪些类型你迟早会遇到?
| 类型 | 写法 | 第几章 |
|---|---|---|
list | [2.5, 2.4] | 第 06 章 |
tuple | (3.7, 25.0) | 第 11 章 |
dict | {"id": "C001"} | 第 09 章 |
set | {"A", "B"} | 第 09 章 |
NoneType | None(表示"没有值") | 第 10 章 |
numpy.ndarray | np.array([1, 2]) | 第 17 章 |
pandas.DataFrame | pd.read_csv(...) | 第 23 章 |
datetime | pd.to_datetime(...) | 第 29 章 |
None 特别重要:它表示"这里没有值"。函数没有 return 时返回 None; 从字典里取不存在的键用 .get() 也返回 None。第 10、13 章会反复遇到它。
3.4 type() 还是 isinstance()?
两者都能看类型,但用法有区别:
cap_rated = 2.5
cell_id = "C001"
print(type(cap_rated).__name__) # float —— 打印更干净
print(isinstance(cap_rated, float)) # True —— 判断"是不是某一类"
print(isinstance(cap_rated, (int, float))) # True —— 可以一次判断多个类型type(x) == float | isinstance(x, float) | |
|---|---|---|
| 用途 | 精确判断"就是这个类型" | 判断"是这类(含子类)" |
| 推荐度 | 调试时打印用 | 逻辑判断用这个 |
为什么判断要用 isinstance:bool 是 int 的子类,所以 isinstance(True, int) 是 True。 type(True) == int 则是 False。搞混了会在处理"这个值是 0 还是 False"时出问题。
最常用的调试语句(记住它,能解决九成类型问题):
print(type(变量名), 变量名)3.5 类型转换:读文件时天天用
点开:为什么必须转换?(这是本书最重要的伏笔之一)
因为从文件、命令行、网页输入里拿到的东西,永远是字符串。
# 从 CSV 里读出来的 soh 列,长这样:
raw_soh = "0.9921"
print(type(raw_soh)) # <class 'str'>
# 想算它就完蛋了
print(raw_soh - 0.01)
# TypeError: unsupported operand type(s) for -: 'str' and 'float'
# 更隐蔽的坑:* 对字符串是"重复",不是乘法
print(raw_soh * 2) # '0.99210.9921' ← 不报错,但完全不是你要的第二个例子更危险 —— 它不报错。你得到了一串长度翻倍的乱码,还以为自己算出了什么。
所以:读进来的数字必须转类型。
cap = float(raw_soh) # 字符串 → 浮点数
n = int("42") # 字符串 → 整数
s = str(2.5) # 数字 → 字符串第 23 章会有转机:Pandas 的 pd.read_csv() 会自动推断类型,数值列直接变成 float64, 不用手写 float()。这是它相对 csv 模块和 np.loadtxt 最省心的地方。
转换的规则
print(float("2.30")) # 2.3
print(int("42")) # 42
print(int(2.9)) # 2 ← 直接截断,不四舍五入!
print(str(2.5)) # '2.5'
print(bool(0), bool("")) # False False —— 空值是 Falseint(2.9) 得到 2 而不是 3,这是新手最常踩的转换坑。要四舍五入用 round(2.9) 得 3。
转换失败会报 ValueError:
int("abc")
# ValueError: invalid literal for int() with base 10: 'abc'invalid literal 的意思是"这段文字不是合法的字面量" —— 简单说就是"这不是个数字"。
3.6 变量名拼错:NameError
cap_rated = 2.5
print(cap_reate) # 少写了一个 'd'
# NameError: name 'cap_reate' is not definedPython 会把拼错的名字原样回显 —— 这是最快的排查线索。看到 name 'xxx' is not defined 就去对比一下:你写的名字,和定义时的名字,差在哪?
点开:NameError 的三个常见原因
| 原因 | 例子 | 修复 |
|---|---|---|
| 拼写错误 | cap_reate vs cap_rated | 用 IDE 的自动补全,别手打长名字 |
| 字符串没加引号 | cell_id = C001 | 加引号:cell_id = "C001" |
| 忘了先定义 | 用到才第一次赋值,或赋值在条件分支里没走到 | 检查执行顺序 |
第三种最隐蔽:
if False:
x = 1
print(x) # NameError —— x 从来没被赋值过注意:Python 不像 C/Java 那样"声明变量",变量是第一次被赋值时才诞生的。 所以"用到未定义的变量"在 Python 里就是 NameError。
3.7 命名规范:不是为了好看
# 好
cap_rated = 2.5
soh_now = 0.92
n_cycles_to_eol = 862
# 坏(能跑,但读代码的人会痛苦)
a = 2.5
data2 = 0.92
CR = 862
容量 = 2.5| 规则 | 说明 |
|---|---|
| 小写 + 下划线 | cap_rated 而不是 CapRated(那是类名风格,第 15 章) |
| 全大写表示常量 | RATED_CAP = 2.5、EOL_SOH = 0.80 |
| 见名知义 | n_cycles 比 n 好;soh_prev 比 sp 好 |
| 不用拼音缩写 | rongliang 不如 capacity |
| 别用中文名 | 语法允许,但团队协作、搜索引擎、字体都可能出问题 |
为什么值得花时间想名字:代码被读的次数远多于被写的次数。 一个能自解释的名字,等于省下了一行注释 —— 而且注释会过时,名字不会。
注意 n_ 前缀:这是"数量"的惯例写法(n_cycles、n_cells),源自数学里的 n。
3.8 完整示例与输出
运行:
cd code
python demos/ch03-types.py真实输出(节选):
=== ① 变量与 f-string ===
电芯 C001:额定容量 2.5 Ah,电压 3.7 V,温度 25 ℃
是新电芯吗?True
=== ② 变量是标签,不是盒子 ===
类型:a = float ,b = float
a 和 b 指向同一个对象吗? True
=== ③ type() 与 isinstance() ===
cap_rated 2.5 float isinstance(float)? True
cell_id C001 str isinstance(float)? False
=== ④ 类型转换(读文件时天天用) ===
str 转 float: 2.3 float
float 转 int: 2 ← 直接截断,不四舍五入!
数字与字符串相加会报错:
TypeError: unsupported operand type(s) for +: 'float' and 'str'
=== ⑤ 拼错变量名会怎样 ===
NameError: name 'cap_reate' is not defined点开:`a is b` 为什么是 True?和 `==` 有什么区别?
a = 2.5
b = a
print(a is b) # True —— 同一个对象
print(a == b) # True —— 值相等两个运算符问的是完全不同的问题:
| 运算符 | 问题 | 类比 |
|---|---|---|
== | 两个对象的值相等吗? | 两份复印件内容一样吗? |
is | 是同一个对象吗? | 是同一张纸吗? |
用哪个:
- 比较值 → 用
==(soh == 0.92) - 判断
None→ 用is(if x is None:)—— 这也是 PEP 8 的要求
if result is None: # ✅ 推荐
if result == None: # ❌ 能跑,但不推荐为什么 is None 更好:None 在整个 Python 里只有一个实例, 所以"是不是同一个对象"这个判断最快也最准确。而 == 可以被自定义(第 15 章的 __eq__), 理论上会返回意外结果。
⚠ 别用 is 比较数字:小整数有缓存机制,a = 256; b = 256; a is b 是 True, 但 a = 257; b = 257; a is b 可能是 False。这种依赖实现细节的写法是 bug 温床。
本章练习
练习 1:用变量描述一支电芯
在 demos/ch03-types.py 的基础上加一个变量 cell_id = "C001",并打印成:
电芯 C001:额定容量 2.5 Ah要求:写出完整代码,然后自己跑一遍确认输出。
参考答案
cap_rated = 2.5 # 额定容量(Ah)
voltage = 3.7 # 标称电压(V)
temp = 25 # 温度(℃)
print(f"额定容量 {cap_rated} Ah,电压 {voltage} V,温度 {temp} ℃")
# ① 字符串必须带引号,否则 Python 会把它当成一个(不存在的)变量名 → NameError
cell_id = "C001"
# ② f-string 里直接写变量名
print(f"电芯 {cell_id}:额定容量 {cap_rated} Ah")
# ③ 顺手确认类型
print(f"(校验)cell_id 的类型是 {type(cell_id).__name__}")三个要点:
- 字符串的引号:
"C001"是字符串,C001是变量名。少写引号是本练习唯一的坑。 type(x).__name__打印更干净(得到str而不是<class 'str'>)。- 变量名拼错(如
cap_reate)会报NameError: name 'cap_reate' is not defined。
完整练习在 exercises/ex04-03-types —— 含骨架 exercise.py 和参考答案 solution.py。
练习 2:亲手制造三种错误
不要直接看答案。依次写下这三行并运行,记下报错信息,再说出原因:
print(cap_reate) # ①
print(2.5 + "3") # ②
cell_id = C001 # ③参考答案
① NameError: name 'cap_reate' is not defined 变量没定义。原因通常是拼写错误 —— Python 会原样回显你写的名字,对比一下定义处的名字即可。
② TypeError: unsupported operand type(s) for +: 'float' and 'str'float 和 str 不能相加。+ 对字符串是"拼接",对数字是"加法",Python 不会自动帮你把 "3" 变成 3。 修复:2.5 + float("3") 或 2.5 + 3。
③ NameError: name 'C001' is not defined注意这个报错和 ① 一模一样 —— 因为没加引号的 C001 被当成变量名了。 这是新手最常犯的"字符串忘加引号"错误,而报错信息不会告诉你"你少了个引号"。
读报错的三步(第 13 章会详细讲):
- 最后一行 → 什么错(
NameError/TypeError) - 倒数第二段
File "..."→ 错在哪一行 ^箭头 → 具体哪个字符
记住:报错信息是从下往上看的。最上面那段 Traceback 只是调用栈,先别看。
练习 3:预测输出(不要运行,先想)
a = 2.5
b = a
b = 3.0
print(a, b)
x = [1, 2]
y = x
y.append(3)
print(x, y)要求:先写下你认为的输出,再运行验证。如果不一致,解释为什么。
参考答案
预测:
2.5 3.0
[1, 2, 3] [1, 2, 3]第一组为什么 a 没变?b = 3.0 是重新贴标签 —— 把 b 这个标签从 2.5 那个对象上撕下来,贴到 3.0 上。 a 还贴在 2.5 上,完全没受影响。
a → [2.5] b = a 之后: a → [2.5] ← b
b = 3.0 之后: a → [2.5] b → [3.0]第二组为什么 x 变了?y.append(3) 没有重新赋值,而是通过 y 找到了那个列表对象,就地修改了它。 x 和 y 贴的是同一个对象,所以都能看到变化。
x → [[1,2]] y = x 之后: x → [[1,2]] ← y
y.append(3) 之后:x → [[1,2,3]] ← y (同一个对象被改了)★ 关键区别:
| 操作 | 效果 |
|---|---|
b = 3.0 | 重新绑定(撕标签、贴新对象)→ 不影响别人 |
y.append(3)、y[0] = 9 | 原地修改(改的就是那个对象)→ 所有引用者都看到变化 |
怎么判断"这个操作会不会原地改":
- 有返回值的,通常是"产生新对象"(
a + b、sorted(x)、y.copy()); - 返回
None的,通常是"原地修改"(y.append()、y.sort()、y.pop())。
这条规律在 Pandas 里同样成立:df.sort_values(...) 返回新表, df.sort_values(..., inplace=True) 原地改(本书不推荐后者,容易搞不清手上是哪一版)。
面向 AI 开发:类型为什么是 AI 项目的第一号 bug 来源
AI 项目里最常见的运行时错误,就是类型不对。
# 这三个都是真实项目里反复出现的 bug:
loss = "0.234" # ① 从日志里读出来的,是字符串
loss = loss - 0.1 # TypeError
x = np.array([1, "2"]) # ② 一列里混了字符串
x * 2 # 整列变成字符串运算,全错
y = df["label"] # ③ 标签列是字符串 "0"/"1"
y.astype(np.float32) # 忘了这一句,模型直接报错三个实用的防御习惯:
① 在数据入口处一次性转干净,别在中间到处转。
# ❌ 错的:边用边转,漏一个就崩
soh = float(row["soh"])
...
cap = row["capacity_ah"] * 2.5 # 忘了转,字符串 * 数字 = 重复
# ✅ 对的:入口处统一转,之后都是干净数字
NUM_COLS = ["capacity_ah", "soh", "resistance_mohm"]
for c in NUM_COLS:
df[c] = df[c].astype(float) # 或者交给 pd.read_csv 自动推断② 用类型注解 + 静态检查提前发现问题。
第 16 章会讲:给函数加上 -> float 这样的注解,再用 mypy 或 ruff 检查, 不用运行代码就能发现"你把字符串传给了要 float 的参数"。
③ 在数据管道里加断言。
assert df["soh"].dtype == float, f"soh 的类型不对:{df['soh'].dtype}"
assert df["soh"].between(0, 1).all(), "SOH 超出 [0, 1] 范围"为什么这在 AI 里格外重要:模型的输入是一个巨大的数值张量。 如果某一列悄悄变成了字符串,你可能要等到训练跑完、看结果不对才发现 —— 而如果那一刻你在做的是 8 小时的训练,代价就是 8 小时。
"入口处转类型 + 断言校验"这两招,能省掉大量这种时间。