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 的核心差异,方便你对照记忆:

Python 2 → 3 核心差异总览 Python 2 → 3 迁移 print 语句 → 函数 整数除法 / → // Unicode 字符串 异常语法 print 函数化 • 必须加括号 • 支持 sep/end/file/flush • 可作为参数传递 除法行为 • / 返回浮点数 • // 返回整数 • 不再隐式截断 Unicode 统一 • str = Unicode • bytes 处理二进制 • len 返回字符数 异常语法 • as 替代逗号 • raise ... from 链 • 必须继承 BaseException 核心原则:明确优于隐式,减少歧义 迁移时重点关注这四个方面,覆盖 90% 的兼容性问题

3.6 迁移实战建议

最后,给你几个实操建议:

  1. 先用工具扫描:2to3 或 futurize 能自动处理大部分语法差异
  2. 重点检查除法:全局搜索 /,逐个确认是否需要 //
  3. 字符串加前缀:Python 2 的 u"..." 在 Python 3 里依然合法,迁移时可以先保留
  4. 异常语法统一:把 except ... , e 全部改成 except ... as e
  5. 写测试用例:覆盖边界情况,比如空字符串、负数除法、Unicode 特殊字符

我的经验:迁移时不要一次性改完所有代码。先跑通核心模块,再逐步扩展。我曾经用 python -3 选项在 Python 2 环境下运行,它能提前警告即将废弃的语法,这个技巧帮我省了不少事。