Python 2 与 Python 3 的核心差异
Python 3.0 于 2008 年发布,是一次不兼容的彻底重构。Guido van Rossum 和核心开发团队认为,Python 2 积累的一些设计缺陷(尤其是字符串处理)已经严重到必须"破釜沉舟"才能修正。理解这些差异,是阅读 Python 2 代码和向 Python 3 迁移的基础。
print:从语句到函数
在 Python 2 中,print 是一个语句,语法类似自然语言:
print "Hello, world"
print "The answer is", 42 # 多个对象用逗号分隔,自动加空格
print "No newline", # 末尾逗号取消换行
Python 3 中 print 变成了函数,必须加括号:
print("Hello, world")
print("The answer is", 42)
print("No newline", end="") # 用 end 参数控制换行
这个改动看似微小,实则影响深远。作为函数,print 可以接受关键字参数(sep、end、file),可以被赋值给变量、作为参数传递,甚至可以用 lambda 包装。作为语句,它不具备这些灵活性。
输入函数:raw_input 与 input
Python 2 有两个输入函数,这是初学者最容易混淆的地方:
name = raw_input("Enter your name: ") # 始终返回字符串
age = input("Enter your age: ") # 等价于 eval(raw_input()),会执行输入内容!
raw_input() 安全且直观:用户输入什么,就返回什么字符串。input() 则危险得多:它把用户的输入当作 Python 表达式执行。如果用户输入 __import__('os').system('rm -rf /'),程序真的会尝试执行这条命令。
Python 3 统一了这两个函数:input() 就是原来的 raw_input(),而旧的 input() 行为被彻底移除。
字符串:str 与 unicode 的分裂
Python 2 中有两种字符串类型,这是它最复杂也最致命的设计:
s = "hello" # str 类型,存储字节序列(默认 ASCII 编码)
u = u"你好" # unicode 类型,存储 Unicode 字符
str 本质上是字节串,每个字符对应一个字节(在 ASCII 范围内)。一旦涉及中文、日文等非 ASCII 字符,就必须显式使用 u"..." 前缀创建 unicode 对象。混用两者会引发著名的 UnicodeDecodeError:
s = "你好" # 如果文件头没有 # -*- coding: utf-8 -*-,这里就会报错
u = u"你好"
print s + u # UnicodeDecodeError: ascii codec can't decode...
Python 3 彻底解决了这个问题:str 就是 Unicode 字符串,而字节串用独立的 bytes 类型表示。两者不能隐式混用,必须显式编码/解码:
# Python 3
s = "你好" # str,Unicode
b = s.encode('utf-8') # bytes,显式编码
s2 = b.decode('utf-8') # str,显式解码
整数除法:地板除的陷阱
Python 2 中,/ 运算符的行为取决于操作数的类型:
print 17 / 3 # 5,两个整数相除,结果截断为整数
print 17 / 3.0 # 5.66666666667,只要有一个浮点数,结果就是浮点数
print 17 // 3 # 5,地板除(// 在 Python 2 和 3 中行为一致)
这种"根据操作数类型改变行为"的设计极易引发 bug。Python 3 中 / 始终执行真除法(返回浮点数),// 始终执行地板除。Python 2.7 提供了 from __future__ import division 来提前体验 Python 3 的行为:
from __future__ import division
print 17 / 3 # 5.66666666667
print 17 // 3 # 5
range 与 xrange
Python 2 中 range(1000000) 会立刻创建一个包含一百万个整数的列表,消耗大量内存。xrange(1000000) 则返回一个惰性迭代器,只在需要时生成下一个数字:
for i in range(1000000): # 先分配一大块内存,再遍历
pass
for i in xrange(1000000): # 几乎不占用额外内存
pass
Python 3 中 range() 就是 Python 2 的 xrange(),而 xrange 这个名字被移除了。
字典方法:items 与 iteritems
Python 2 的字典提供了两套方法,分别返回列表和迭代器:
d = {'a': 1, 'b': 2}
print d.items() # [('a', 1), ('b', 2)],返回完整列表
print d.iteritems() # <dictionary-itemiterator>,返回迭代器
遍历大字典时,iteritems() 更省内存。Python 3 中 items() 直接返回视图(类似迭代器),iteritems() 被移除。
异常语法:逗号与 as
Python 2 支持两种捕获异常的语法:
try:
1 / 0
except ZeroDivisionError, e: # 传统语法,逗号分隔
print "Error:", e
except ZeroDivisionError as e: # Python 2.6+ 引入,与 Python 3 一致
print "Error:", e
逗号语法在 Python 3 中被移除,因为它容易与元组字面量混淆:except (A, B), e 看起来像是捕获两个异常并赋值给 e,实际上在旧语法中确实如此,但可读性极差。
类:旧式类与新式类
Python 2 中,不继承任何类的定义是"旧式类":
class OldStyle: # 旧式类,不继承 object
pass
class NewStyle(object): # 新式类,显式继承 object
pass
旧式类的方法解析顺序(MRO)是深度优先,不支持 super()、property、描述符等现代特性。Python 3 中所有类都是新式类,object 继承是隐式的。
其他差异速查
| 特性 | Python 2.7 | Python 3.x |
|---|---|---|
| 内置模块名 | __builtin__ | builtins |
| 编译文件 | .pyc/.pyo 与源文件同目录 | __pycache__/*.pyc |
| 整数类型 | int(有限)/ long(无限) | 统一为 int(无限精度) |
| 比较不同类型 | 3 < "a" 合法(按类型名比较) | 抛出 TypeError |
| next 方法 | .next() | .__next__() |
| 列表推导式变量泄漏 | for x in ... 的 x 泄漏到外部作用域 | 不泄漏 |
| 编码声明 | 必须写 # -*- coding: utf-8 -*- | 默认 UTF-8 |
迁移工具:2to3
Python 官方提供了 2to3 脚本,可以自动将大部分 Python 2 代码转换为 Python 3:
$ 2to3 -w myscript.py # -w 表示直接写入文件
$ 2to3 -w mypackage/ # 可以处理整个目录
但它不能处理所有情况,尤其是涉及字节/字符串边界、元类、C 扩展的代码。理解上述差异,才能判断 2to3 的转换是否正确,并手动修复它遗漏的问题。