Skip to content

第 03 章 · 变量、对象与数据类型 ​

这一章要解决什么:把一支电芯的参数存进变量并打印出来。 听起来简单,但这里面藏着三个后面会反复出现的基础认知:变量是"标签"、类型决定能做什么、类型转换。

本章你需要完成的代码:demos/ch03-types.py

3.1 变量:给值起个名字 ​

python
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 看到的东西也变了

验证:

python
a = 2.5
b = a
print(a is b)     # True —— is 判断"是不是同一个对象"

为什么 Python 这么设计:复制大对象(比如一个 10 万行的 DataFrame)代价很高。 默认"共享",需要复制时用 .copy() 明确说出来 —— 这是"显式优于隐式"的体现。

这条认知的直接后果(第 06 章会演示):

python
a = [1, 2, 3]
b = a              # 不是复制!
b.append(4)
print(a)           # [1, 2, 3, 4] —— a 也变了

3.3 类型:决定这个值能做什么 ​

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

为什么类型重要:它决定了这个值能做什么运算。

python
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 章
NoneTypeNone(表示"没有值")第 10 章
numpy.ndarraynp.array([1, 2])第 17 章
pandas.DataFramepd.read_csv(...)第 23 章
datetimepd.to_datetime(...)第 29 章

None 特别重要:它表示"这里没有值"。函数没有 return 时返回 None; 从字典里取不存在的键用 .get() 也返回 None。第 10、13 章会反复遇到它。

3.4 type() 还是 isinstance()? ​

两者都能看类型,但用法有区别:

python
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) == floatisinstance(x, float)
用途精确判断"就是这个类型"判断"是这类(含子类)"
推荐度调试时打印用逻辑判断用这个

为什么判断要用 isinstance:bool 是 int 的子类,所以 isinstance(True, int) 是 True。 type(True) == int 则是 False。搞混了会在处理"这个值是 0 还是 False"时出问题。

最常用的调试语句(记住它,能解决九成类型问题):

python
print(type(变量名), 变量名)

3.5 类型转换:读文件时天天用 ​

点开:为什么必须转换?(这是本书最重要的伏笔之一)

因为从文件、命令行、网页输入里拿到的东西,永远是字符串。

python
# 从 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'  ← 不报错,但完全不是你要的

第二个例子更危险 —— 它不报错。你得到了一串长度翻倍的乱码,还以为自己算出了什么。

所以:读进来的数字必须转类型。

python
cap = float(raw_soh)        # 字符串 → 浮点数
n = int("42")               # 字符串 → 整数
s = str(2.5)                # 数字 → 字符串

第 23 章会有转机:Pandas 的 pd.read_csv() 会自动推断类型,数值列直接变成 float64, 不用手写 float()。这是它相对 csv 模块和 np.loadtxt 最省心的地方。

转换的规则 ​

python
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  —— 空值是 False

int(2.9) 得到 2 而不是 3,这是新手最常踩的转换坑。要四舍五入用 round(2.9) 得 3。

转换失败会报 ValueError:

python
int("abc")
# ValueError: invalid literal for int() with base 10: 'abc'

invalid literal 的意思是"这段文字不是合法的字面量" —— 简单说就是"这不是个数字"。

3.6 变量名拼错:NameError ​

python
cap_rated = 2.5
print(cap_reate)          # 少写了一个 'd'
# NameError: name 'cap_reate' is not defined

Python 会把拼错的名字原样回显 —— 这是最快的排查线索。看到 name 'xxx' is not defined 就去对比一下:你写的名字,和定义时的名字,差在哪?

点开:NameError 的三个常见原因
原因例子修复
拼写错误cap_reate vs cap_rated用 IDE 的自动补全,别手打长名字
字符串没加引号cell_id = C001加引号:cell_id = "C001"
忘了先定义用到才第一次赋值,或赋值在条件分支里没走到检查执行顺序

第三种最隐蔽:

python
if False:
    x = 1
print(x)        # NameError —— x 从来没被赋值过

注意:Python 不像 C/Java 那样"声明变量",变量是第一次被赋值时才诞生的。 所以"用到未定义的变量"在 Python 里就是 NameError。

3.7 命名规范:不是为了好看 ​

python
# 好
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 完整示例与输出 ​

运行:

bash
cd code
python demos/ch03-types.py

真实输出(节选):

text
=== ① 变量与 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?和 `==` 有什么区别?
python
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 的要求
python
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

要求:写出完整代码,然后自己跑一遍确认输出。

参考答案
python
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__}")

三个要点:

  1. 字符串的引号:"C001" 是字符串,C001 是变量名。少写引号是本练习唯一的坑。
  2. type(x).__name__ 打印更干净(得到 str 而不是 <class 'str'>)。
  3. 变量名拼错(如 cap_reate)会报 NameError: name 'cap_reate' is not defined。

完整练习在 exercises/ex04-03-types —— 含骨架 exercise.py 和参考答案 solution.py。

练习 2:亲手制造三种错误

不要直接看答案。依次写下这三行并运行,记下报错信息,再说出原因:

python
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 章会详细讲):

  1. 最后一行 → 什么错(NameError / TypeError)
  2. 倒数第二段 File "..." → 错在哪一行
  3. ^ 箭头 → 具体哪个字符

记住:报错信息是从下往上看的。最上面那段 Traceback 只是调用栈,先别看。

练习 3:预测输出(不要运行,先想)
python
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 项目里最常见的运行时错误,就是类型不对。

python
# 这三个都是真实项目里反复出现的 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)                # 忘了这一句,模型直接报错

三个实用的防御习惯:

① 在数据入口处一次性转干净,别在中间到处转。

python
# ❌ 错的:边用边转,漏一个就崩
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 的参数"。

③ 在数据管道里加断言。

python
assert df["soh"].dtype == float, f"soh 的类型不对:{df['soh'].dtype}"
assert df["soh"].between(0, 1).all(), "SOH 超出 [0, 1] 范围"

为什么这在 AI 里格外重要:模型的输入是一个巨大的数值张量。 如果某一列悄悄变成了字符串,你可能要等到训练跑完、看结果不对才发现 —— 而如果那一刻你在做的是 8 小时的训练,代价就是 8 小时。

"入口处转类型 + 断言校验"这两招,能省掉大量这种时间。


下一章:第 04 章 · 数值与算术:算一支电芯的 SOH