3. Python 2 与 3 核心差异:print 语句 vs 函数、整数除法、Unicode 字符串、异常语法
从 Python 2 迁移到 Python 3,说白了就是一场「语法大扫除」。很多老代码跑不起来,不是因为逻辑错了,而是因为 Python 3 把一些「历史包袱」给扔了。我当年接手一个遗留系统时,光改 print 就改了一整天。嗯,今天咱们就把这几个最要命的差异讲透。
核心观点:Python 3 的设计哲学是「明确优于隐式」。所有改动都围绕这个原则展开——让代码更清晰、更少歧义。
3.1 print:从语句到函数
这是最直观的变化。Python 2 里 print 是个语句,不需要括号:
# Python 2
print "Hello, World!"
print "The answer is", 42
Python 3 里 print 变成了函数,必须加括号:
# Python 3
print("Hello, World!")
print("The answer is", 42)
你可能会想:「不就是加个括号吗?」其实没那么简单。函数化带来了几个实实在在的好处:
- 可以赋值给变量:
p = print; p("hello")在 Python 3 里合法 - 可以作为参数传递:比如
list(map(print, [1,2,3])) - 支持关键字参数:
sep、end、file、flush
我个人最常用的是 sep 和 end:
# 用逗号分隔,而不是空格
print("a", "b", "c", sep=",") # 输出: a,b,c
# 不换行,末尾加个空格
print("loading", end=" ")
print("done") # 输出: loading done
迁移技巧:如果你还在维护 Python 2 代码,可以用 from __future__ import print_function 提前启用 Python 3 的 print 行为。这样迁移时少改一个坑。
3.2 整数除法:/ 和 // 的彻底分家
这个坑我踩过不止一次。Python 2 里 5 / 2 = 2,因为整数除法直接截断小数。Python 3 里 5 / 2 = 2.5,返回浮点数。
为什么会这样?因为 Python 2 的设计让很多人写出隐晦的 bug。你想想看,一个函数里算平均值,结果因为输入是整数就丢了小数部分——这种问题排查起来特别痛苦。
| 操作 | Python 2 | Python 3 |
|---|---|---|
5 / 2 |
2(截断) | 2.5(浮点) |
5 // 2 |
2(截断) | 2(截断) |
5.0 / 2 |
2.5 | 2.5 |
5.0 // 2 |
2.0 | 2.0 |
Python 3 的规则其实更简单:/ 永远返回浮点数,// 永远做整除。我建议你养成习惯:
- 想要浮点结果 → 用
/ - 想要整数结果 → 用
// - 不要依赖隐式类型转换
避坑指南:我曾经在迁移一个金融计算模块时,因为 / 的行为变化导致利息计算结果全部偏差。排查了整整半天才发现是除法问题。建议迁移时全局搜索 /,逐个确认是否需要改成 //。
3.3 Unicode 字符串:告别 ASCII 时代
Python 2 的字符串系统是个历史遗留问题。它有两种类型:str(字节串)和 unicode(Unicode 字符串)。Python 3 统一了:所有字符串都是 Unicode,bytes 类型专门处理二进制数据。
看个对比:
# Python 2
type("hello") # <type 'str'> —— 其实是字节串
type(u"hello") # <type 'unicode'>
# Python 3
type("hello") # <class 'str'> —— 现在是 Unicode
type(b"hello") # <class 'bytes'> —— 二进制数据
这个改动影响深远。我举个例子:
# Python 2 里,len("中文") 返回 6(UTF-8 编码后 6 字节)
# Python 3 里,len("中文") 返回 2(2 个字符)
# Python 3 的正确做法
text = "中文"
print(len(text)) # 2
print(len(text.encode())) # 6 —— 显式编码后才算字节数
个人习惯:我现在写代码,所有文件开头都加 # -*- coding: utf-8 -*-,虽然 Python 3 默认就是 UTF-8,但加了这个注释能让团队里还在用 Python 2 的同事少踩坑。
3.4 异常语法:更简洁、更安全
Python 3 对异常语法做了三处改动,每一处都让代码更干净。
3.4.1 捕获语法:as 替代逗号
# Python 2
try:
1 / 0
except ZeroDivisionError, e:
print e
# Python 3
try:
1 / 0
except ZeroDivisionError as e:
print(e)
用 as 更清晰,不会和元组解包混淆。我见过 Python 2 代码里 except (TypeError, ValueError), e 这种写法,新手很容易看晕。
3.4.2 异常链:raise ... from
Python 3 引入了异常链机制,能保留原始异常上下文:
try:
int("abc")
except ValueError as e:
raise RuntimeError("转换失败") from e
这样抛出的异常会同时显示原始 ValueError 和当前 RuntimeError,调试时能直接看到根因。我在做 API 封装时特别喜欢用这个特性。
3.4.3 异常必须是 BaseException 子类
Python 2 允许抛出任意对象:
# Python 2 合法
raise "error"
Python 3 只允许抛出 BaseException 的子类实例。这个改动杜绝了「抛字符串」这种坏习惯。
我曾经在维护一个老项目时,发现有人用 raise "Timeout" 来终止流程。迁移到 Python 3 后直接报错。建议迁移前全局搜索 raise,确保所有异常都是合法的异常类实例。
3.5 知识体系总览
下面这张图总结了 Python 2 到 3 的核心差异,方便你对照记忆:
3.6 迁移实战建议
最后,给你几个实操建议:
- 先用工具扫描:
2to3或futurize能自动处理大部分语法差异 - 重点检查除法:全局搜索
/,逐个确认是否需要// - 字符串加前缀:Python 2 的
u"..."在 Python 3 里依然合法,迁移时可以先保留 - 异常语法统一:把
except ... , e全部改成except ... as e - 写测试用例:覆盖边界情况,比如空字符串、负数除法、Unicode 特殊字符
我的经验:迁移时不要一次性改完所有代码。先跑通核心模块,再逐步扩展。我曾经用 python -3 选项在 Python 2 环境下运行,它能提前警告即将废弃的语法,这个技巧帮我省了不少事。