返回首页
## 引言
这是第二章「变量、类型与运算符」的第 6 篇。前 5 篇我们学习了:变量绑定(名字只是对象的引用,第 1 篇)、数字类型的完整画像(int/float/complex,第 2 篇)、布尔值与真值测试(第 3 篇)、字符串字面量与转义、原始字符串(第 4 篇)、f-string 格式化(第 5 篇)。你已经能在不同类型之间"感知"到差异——整数可以精确相等,浮点数会显示成 `0.30000000000000004`,字符串有引号。本篇回答一个问题:数据如何从一种类型变成另一种类型?两种途径——显式调用构造函数,以及运算中解释器悄悄完成的隐式转换。这些机制每天都在发生,但只有理解了边界,你写的程序才不会在生产环境突然崩溃。阅读本篇前,你应熟练掌握前 5 篇的概念,尤其是「对象不可变、赋值是重新绑定」这一底层模型。
## 概念与原理:转换的本质是构造新对象
先建立最重要的直觉:**Python 中的类型转换不是"修改",而是"构造"**。int 对象、float 对象、str 对象一旦创建就不可变。`int(3.9)` 并没有把那个 3.9 的浮点对象"截断成 3",而是基于 3.9 的数值,在内存中构造了一个全新的 int 对象 `3`,并让名字绑定到它。这与第 1 篇讲的「b = a 之后修改 a 不影响 b」完全同源——转来转去,老的绑定纹丝不动。
按触发方式,转换分两类:
- **显式转换(explicit conversion)**:你主动调用类型构造函数,如 `int(x)`、`float(x)`、`str(x)`、`bool(x)`、`complex(x)`。
- **隐式转换(implicit conversion)**:二元运算时解释器自动把操作数统一成"更宽"的类型,如 `1 + 2.0` 中整数被提升为浮点。
隐式转换有个重要方向:**int → float → complex**。`1 + 2j` 之类混合运算的结果类型,永远取"金字塔"里更靠右的那一个。为什么是单向的?因为 float 能表示 int 的全部常用数值范围,complex 又能包容 float,统一成宽类型不会(在大多数情况下)丢失数值本身。但注意——"更宽"不等于"更精确":float 只有约 15~17 位十进制有效精度,超大整数一旦掺进浮点运算就可能丢位(见下文易错点 4)。
## 操作与实现:三类构造函数的行为总览
### 数字构造函数:从数值对象转换
```python
print(int(-3.7)) # -3,向零截断(不是四舍五入)
print(int(True)) # 1,布尔参与数字转换
print(float(2)) # 2.0
print(complex(3, 4)) # (3+4j):由实部虚部构造
```
`int()` 接收 float 时采用**向零截断(truncation toward zero)**:正数向下取整、负数向上取整,`int(2.9)` 得 2,`int(-2.9)` 得 -2。这与 `round()` 的四舍五入不是一回事,极易混淆,后面易错点 3 专门展开。
### 字符串转换:int() 的进制参数
`int()` 最重要的重载是接收字符串。当目标是"解析用户输入或配置文件文本"时,字符串是唯一输入形式:
```python
print(int("12")) # 12,十进制
print(int(" 12 ")) # 12,首尾空白被忽略
print(int("-1_000")) # -1000,支持下划线分隔符
print(int("0xFF", 0)) # 255,base=0 表示按前缀自行推断
```
关键的第二个参数 `base`(进制基数,2~36)让字符串解析变得极其灵活。它在文本处理里是常客:读取二进制协议字段、解析十六进制颜色、处理网络数据包时,你几乎天天与它打交道:
```python
print(int("10", 2)) # 2,把 "10" 按二进制理解
print(int("10", 16)) # 16,把 "10" 按十六进制理解
print(int("0b101", 0)) # 5,base=0 时 "0b" 前缀自动识别
```
注意 `int("10", 2)` 与 `int("10", 16)` 的输入字符串完全相同,结果却不同——**进制只影响解释方式,与字符串本身无关**。这是初学者最常见的困惑点之一。
### 字符串转换:float() 与 complex()
`float()` 接受的字符串比 `int()` 宽松得多,因为它要覆盖科学计数法与特殊值:
```python
print(float("1e3")) # 1000.0,科学计数法
print(float("3.0")) # 3.0,带小数点的写法
print(float("inf")) # inf,正无穷
print(float("nan")) # nan,非数值
print(complex("1+2j")) # (1+2j),复数只有一种解析格式
```
## 字符串转数字的 ValueError 边界
理解"哪些字符串合法"比背诵错误消息更重要。`int()` 的合法输入是:**可选正负号 + 空白 + 十进制整数文本(可带下划线)**。它**不接受**小数点、指数形式,即使数值上"恰好是整数"也不行——这是设计取舍:`int()` 认的是**整数语法**,而不是"能四舍五入成整数的任何写法":
```python
def safe_int(s: str):
"""把字符串转 int,失败时返回带原因的描述。"""
try:
return int(s)
except ValueError as e:
return f"ValueError: {e}"
print(safe_int("1.5")) # ValueError: invalid literal for int() with base 10: '1.5'
print(safe_int("3.0")) # ValueError: invalid literal for int() with base 10: '3.0'
print(safe_int("1e3")) # ValueError: invalid literal for int() with base 10: '1e3'
print(safe_int("")) # ValueError: invalid literal for int() with base 10: ''
print(safe_int("1")) # 1,正常路径
```
三个失败例子的共同点是:文本包含小数点或 `e`,不属于十进制整数的字面量语法,于是抛出 `ValueError`。对应的正确姿势是分两步——若用户输入的是 `"3.14"`,先 `float(s)` 再 `int()`;若输入 `"1e3"`,同样先过 `float()`。把"解析失败"与"数值越界"区分开还有个细节:`int("2" * 500)` 不会报错(Python 整数任意精度),但 `float("1e400")` 会返回 `inf` 而不是报错,设计上 float 就是"溢出→无穷"。
还有两个 float 特有的边界需要记住,它们的报错类型都不一样:
```python
try:
int(float("nan"))
except ValueError as e:
print("ValueError:", e) # cannot convert float NaN to integer
try:
int(float("inf"))
except OverflowError as e:
print("OverflowError:", e) # cannot convert float infinity to integer
```
NaN(not a number)与无穷大无法落进整数,`int()` 分别走 `ValueError` 与 `OverflowError` 两条通道。实战中它们经常从解析失控数据(如除零、空值运算)传来,捕获时按类型分别处理,日志里才能看出问题本质。
### 数字转字符串:反向旅程
反向转换同样重要。`str()` 对 int 是精确无损的;对 float 则输出**最短表示**——保证 `float(str(x)) == x` 往返成立,所以 `str(0.1)` 是 `"0.1"` 而不是那串 0.30000000000000004 式的长尾巴。`bool` 转字符串更特别:`str(True)` 是 `"True"`,不是 `"1"`;而 `int(True)` 才是 1。文本展示与数值语义,二者井水不犯河水:
```python
print(str(0.1), str(True), str(3.14)) # 0.1 True 3.14
print(str(1e-7), str(1e-8)) # 1e-07 1e-08,小浮点用科学计数法
print(float(str(0.1)) == 0.1) # True,往返一致
```
把数字拼进字符串是工程高频动作,但 `+` 只接受字符串,硬拼直接抛 TypeError:`"总分: " + 3` 报 `can only concatenate str (not "int") to str`。正确做法是 f-string(第 5 篇)或显式 `str(3)`。这也解释了为什么 `"3" + "4"` 是 `"34"` 而 `3 + 4` 是 `7`——运算符不做隐式类型转换,这是 Python 与不少脚本语言(如 JavaScript 的 `"3" + 4` 得 `"34"`)刻意不同的行为:宁可报错,也不静默猜你的意图。
### 一个完整的工程片段:解析成绩文件
把本篇的知识串成一个真实场景:读取 `input()` 得到的成绩行并计算总分。`input()` 返回的永远是字符串,因此"字符串 → 数字 → 运算 → 格式化输出"是必经之路:
```python
line = input("请输入三科成绩,以逗号分隔(如 90,75,60):")
parts = line.split(",") # 字符串切分
try:
scores = [float(p.strip()) for p in parts] # 逐个转数字
except ValueError:
print("输入格式错误:存在无法解析的数字")
else:
total = sum(scores) # 数字才可求和
print(f"总分 {total:.1f},平均 {total / len(scores):.1f}")
```
这段代码自动用上了本章前半程的全部工具:f-string 格式化(第 5 篇)、真值测试与异常边界(本篇)、以及 `sum()` 对数字的加宽行为。注意 `float(p.strip())` 中先 `strip()` 再转换,正是为了吃掉 `" 90"` 这类带空白的分片——空白合法,但只对字符串两侧;而用户习惯性打出的全角逗号 `"90,75"` 不会被半角 `split(",")` 切开,一整片 `"90,75"` 就会在 `float()` 处炸出 `ValueError`,被 `except` 拦下给出友好提示。这类"用户永远不按你设想的格式输入"的教训,是从脚本走向工具的第一课。
## 隐式转换:数字金字塔与代价
隐式转换在两个场景出现:**数值混合运算**与**布尔混入算术**。演示如下:
```python
print(type(1 + 2.0)) # <class 'float'>,int 被提升为 float
print(type(1 + 2j)) # <class 'complex'>,一切数值最终并入 complex
print(True + True) # 2,bool 是 int 的子类,直接当 1 用
print(sum([True, False, True])) # 2,用布尔列表做计数
```
第 3 篇讲过布尔是 `int` 的子类、`True == 1` 成立——这就解释了一切:布尔参与算术时被自动当作 0/1。这个特性在统计场景很实用,但也会埋下隐患(易错点 2)。
隐式转换的代价是**精度丢失**。float 只有约 15~17 位十进制有效数字,整数的位宽却任意长。两者一相遇,掉队的是大整数:
```python
big = 1_000_000_000_000_000_001 # 10^18 + 1
print(big + 0.0) # 1e+18,末尾的 1 在转换瞬间就被丢弃
```
`big + 0.0` 先把 10^18+1 转成 float,而 float 在 10^18 量级的间隔大于 1,于是 1 没了。这是"更大类型"神话的第一块裂缝。
与之并列的还有二进制浮点的固有误差——`0.1 + 0.2` 在二进制下无法精确表示(第 2 篇提过)。工程上的三个对策依次是:比较用 `math.isclose()` 容差、金额等十进制场景用 `decimal.Decimal`、展示用格式化而不是拼接:
```python
print(0.1 + 0.2) # 0.30000000000000004
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True,容差比较
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2")) # 0.3,十进制精确运算
```
`Decimal("0.1")` 与 `Decimal(0.1)` 也不同——前者按字符串的十进制理解,后者会先经过那个不精确的二进制 float,务必用字符串构造。
## 易错点与陷阱
先给出一条贯穿全局的检查清单:遇到转换就依次问三个问题——**转换方向对不对**(数字转字符串用 `str()`,字符串转数字按语法选 `int()` 或 `float()`)、**会不会静默丢精度**(涉及大整数、金额时禁止 float 掺和)、**失败时程序怎么死**(`ValueError`/`OverflowError`/`TypeError` 分别出现在哪条路径)。带着清单看下面的案例:
**1. `int()` 直接解析小数形式字符串必炸。** `input()` 拿到的内容永远是字符串。用户输入 `"3.14"` 时 `int(s)` 抛出 ValueError,程序当场终止。正确写法是先 `float()` 再 `int()`,或者用 `try/except` 兜住并给用户友好提示。检验标准很简单:想从文本解析什么"写法",就用接受该写法的构造函数——小数和指数归 `float()`,整数写法归 `int()`。
**2. 布尔混进算术的反直觉:`bool("False")` 是 True。** `bool()` 只做真值测试(第 3 篇),非空字符串恒为真,所以 `bool("False")`、`bool("0")` 都是 `True`。并且由于 `True == 1`,`True + True == 2` 成立。后果一:拿 `sum()` 数布尔会产生"真假混合"的翻译;后果二:把用户字符串转布尔想当然地写 `bool(user_input)`,得到的几乎永远是 True。解析 `"true"/"false"` 文本应显式比较:`user_input.strip().lower() == "true"`。
**3. 截断与舍入不是一回事。** `int(2.9)` 是 2,`int(-2.9)` 是 -2——向零截断;`round(2.5)` 却是 2、`round(3.5)` 是 4——Python 的 round 采用**银行家舍入(banker's rounding)**,即 .5 时舍入到最近的偶数,`round(-2.5)` 得 -2。想对正数做四舍五入,标准写法是 `int(x + 0.5)`;对任意符号则是 `math.floor(x + 0.5)`。
**4. 隐式转换的静默精度丢失。** 大整数与浮点混算、`(1 << 53) + 1` 转 float 等场景,数值悄悄失真且毫无警告。涉及金额、ID、校验和这类"必须精确"的数据,拒绝让 float 掺和;这是选择 `Decimal` 和只做整数运算的第一理由。
## 小结
类型转换分显式与隐式两种:显式是调用 `int/float/str/bool/complex` 构造函数,本质是"构造新对象",绝不修改原对象;隐式发生在数值运算中,方向为 int→float→complex,且布尔可当 0/1 参与算术。字符串转数字的边界是 `int()` 只认整数语法(含 base 参数与下划线),小数点与指数必须交 `float()` 处理。最大代价是精度:float 的有限精度既制造了 `0.1+0.2` 的误差,也吞掉超大整数的低位。下一篇文章把算术、比较、逻辑运算符以及它们的优先级系统地串起来——转换是运算符的"燃料",优先级是"点火顺序"。
## 练习与思考题
1. 写出下列表达式的输出并运行验证:`int("101", 2)`、`int("101", 8)`、`int("101", 16)`、`float("1_000.5")`。为什么输入相同、结果不同?
2. 写一个 `safe_int(s)`,返回 `None` 而非抛异常(提示:除 `ValueError` 外,还需考虑参数根本不是字符串的情形)。
3. `round(0.5)`、`round(1.5)`、`round(2.5)` 的输出是什么?为什么与初等数学的"四舍五入"不同?请用银行家舍入的规则解释。
4. 思考题:`1e308 * 10` 的结果是什么类型、什么值?`int(1e308 * 10)` 又会怎样?运行验证,并说明其与 `float` 溢出设计的联系。