前言
最近 Python 之父 Guido van Rossum(龟爷)终于在 Python 官方邮件组落实了 Python 2.7 的终焉之日(EOL)。
Let’s not play games with semantics. The way I see the situation for 2.7 is that EOL is January 1st, 2020, and there will be no updates, not even source-only security patches, after that date. Support (from the core devs, the PSF, and python.org) stops completely on that date. If you want support for 2.7 beyond that day you will have to pay a commercial vendor. Of course it’s open source so people are also welcome to fork it. But the core devs have toiled long enough, and the 2020 EOL date (an extension from the originally annouced 2015 EOL!) was announced with sufficient lead time and fanfare that I don’t feel bad about stopping to support it at all.
一言以蔽之,说的是 Python 2.7 的 EOL 日期最终确定为 2020 年 1 月 1 日,之后不会有任何更新,包括源码的安全补丁。
So,还没有兼容或移植到 Python 3.x 的程序,或许已经到了该提上日程的时候了。在下文中,我们就「选择 Python 2 还是 3 ?两者的主要区别是什么?如何编写兼容两者的代码?」这 3 个经常在面试中出现的问题来展开。
Python 2 or 3 ?
Python 3 被钦定为 Python 的未来,于 2008 年末发布,是目前正在开发的版本。旨在解决和修正 Python 2 遗留的设计缺陷、清理代码库冗余、追求有且仅有一种最佳实践方式来执行任务等问题。
起初,由于 Python 3 不能向后兼容的事实,导致了用户采用缓慢,对初学者不友好等问题。但在 Python 社区的努力和决绝态度下,截至龟爷发出邮件之前,已经有了 21903 个 Packages 可以支持 Python 3.5,其中包括了绝大多数最受欢迎的封装库,与此同时也有越来越多的封装库(e.g. Django、Numpy)表示其新版本将不再支持 Python 2。
可见时至今日的 Python 3 已成气候,正如龟爷在邮件中所说:「停止支持 Python 2,我完全没觉得有啥糟糕的」。所以笔者认为,如果你喜欢 Python 并希望能够掌握它,那么你完全没有必然去纠结于学习 2 还是 3 的问题。而且当我们在讨论 Python 2.x 的问题时,实际上更多的是在讨论 Python 2.7。
Python 2.7 于 3.0 之后的 2010 年 7 月 3 日发布,计划作为 2.x 的最后一个版本。Python 2.7 的历史任务在于通过提供 2 和 3 之间的兼容性措施,使 Python 2.x 的用户更容易将代码移植到 Python 3.x 上。那么如果你希望自己的代码能够兼容两个不同的版本,首先你起码要让代码能够正常的运行在 Python 2.7 上。
注:下文使用 P2 表示 Python 2.7;使用 P3 表示 Python 3.x。
不同与兼容
__future__ 模块是我们首先需要了解的,该模块最主要的作用是支持在 P2 中导入那些在 P3 才生效的模块和函数。是一个非常优秀的兼容性工具库,在下文中给出的许多 兼容技巧 实例都依赖于它。
特性 | 在此版本可选 | 在此版本内置 | 效果 |
---|---|---|---|
nested_scopes | 2.1.0b1 | 2.2 | PEP 227:静态嵌套作用域 |
generators | 2.2.0a1 | 2.3 | PEP 255:简单生成器 |
division | 2.2.0a2 | 3.0 | PEP 238:除法操作符改动 |
absolute_import | 2.5.0a1 | 3.0 | PEP 328:Imports 多行导入与绝对相对路径 |
with_statement | 2.5.0a1 | 2.6 | PEP 343:with 语句 |
print_function | 2.6.0a2 | 3.0 | PEP 3105:print 语句升级为函数 |
unicode_literals | 2.6.0a2 | 3.0 | PEP 3112:Bytes 类型 |
(__future__ 功能列表)
统一不等于语法
P2 支持使用 <>
和 !=
表示不等于。
P3 仅支持使用 !=
表示不等于。
兼容技巧:统一使用 !=
语法
统一整数类型
P2 中整数类型可以细分为短整型 int 和长整型 long。
P3 废除了短整型,并统一使用 int 表示长整型(不再有 L 跟在 repr 后面)。
兼容技巧:
# Python 2 onlyk = 9223372036854775808L# Python 2 and 3:k = 9223372036854775808# Python 2 onlybigint = 1L# Python 2 and 3from future.builtins import int bigint = int(1)
统一整数除法
P2 的除法 /
符号实际上具有两个功能:
当两个操作数均为整型对象时,进行的是地板除(截除小数部分),返回整型对象;
当两个操作数存在至少一个浮点型对象时,进行的是真除(保留小数部分),返回浮点型对象。
P3 的除法 /
符号仅仅具有真除的功能,而地板除的功能则交由 //
来完成。
兼容技巧:
# Python 2 only:assert 2 / 3 == 0# Python 2 and 3:assert 2 // 3 == 0“True division” (float division):# Python 3 only:assert 3 / 2 == 1.5# Python 2 and 3:from __future__ import division # (at top of module)
统一缩进语法
P2 可以混合使用 tab 和 space 两种方式来进行缩进(1 个 tab == 8 个 space),但实际上这一特性并非所有 IDE 都能够支持,会因此出现同样的代码无法跨 IDE 运行的情况。
P3 统一使用 tab 作为缩进,如果 tab 和 space 同时存在,就会触发异常:
TabError: inconsistent use of tabs and spaces in indentation.
兼容技巧:统一使用 tab 作为缩进。
统一类定义
P2 同时支持新式类(object)和老式类。
P3 则统一使用新式类,并且只有使用新式类才能应用多重继承。
兼容技巧:统一使用新式类。
统一字符编码类型
P2 默认使用 ASCII 字符编码,但因为 ASCII 只支持数百个字符,并不能灵活的满足非英文字符,所以 P2 同时也支持 Unicode 这种更强大的字符编码。不过,由于 P2 同时支持两套字符编码,就难免多出了一些标识和转换的麻烦。
而 P3 统一使用 Unicode 字符编码,这节省了开发者的时间,同时也可以轻松地在程序中输入和显示更多种类的字符。
兼容技巧:在所有的字符串赋值中均使用前缀 u
,或引入 unicode_literals 字符模块。
# Python 2 onlys1 = 'The Zen of Python's2 = u'きたないのよりきれいな方がいい\n'# Python 2 and 3s1 = u'The Zen of Python's2 = u'きたないのよりきれいな方がいい\n'# Python 2 and 3from __future__ import unicode_literals # at top of modules1 = 'The Zen of Python's2 = 'きたないのよりきれいな方がいい\n'
统一导入模块的路径搜索方式
P2 导入一个模块时首先会搜索当前目录(cwd),若非,则搜索环境变量路径(sys.path)。这一特性时常给开发者带来困扰,相信大家都曾经碰到过,尤其当自定义模块与系统模块重名的时候;
为了解决这个问题,默认的 P3 仅会搜索环境变量路径,当你需要搜索自定义模块时,你可以在包管理模式下将项目路径加入到环境变量中,然后再使用绝对路径和相对路径(以 . 开头)的方式来导入。
兼容技巧:统一使用绝对路径进行自定义模块导入。
修正列表推导式的变量作用域泄露
P2 的列表推倒式中的变量会泄露到全局作用域,例如:
import platform print('Python', platform.python_version()) i = 1print('before: I = %s' % i) print('comprehension: %s' % [i for i in range(5)]) print('after: I = %s' % i)# OUTPython 2.7.6before: i = 1comprehension: [0, 1, 2, 3, 4] after: i = 4
P3 则解决了这个问题,列表推倒式中的变量不再泄露到全局作用域。
import platform print('Python', platform.python_version()) i = 1print('before: i =', i) print('comprehension:', [i for i in range(5)]) print('after: i =', i)# OUTPython 3.4.1before: i = 1comprehension: [0, 1, 2, 3, 4] after: i = 1
修正非法比较操作异常
P2 能够对两个数据类型并不相同的对象进行比较。
import platform print('Python', platform.python_version()) print("[1, 2] > 'foo' = ", [1, 2] > 'foo') print("(1, 2) > 'foo' = ", (1, 2) > 'foo') print("[1, 2] > (1, 2) = ", [1, 2] > (1, 2))# OUTPython 2.7.6[1, 2] > 'foo' = False(1, 2) > 'foo' = True[1, 2] > (1, 2) = False
不过,这种看似方便的特性,实际上却是一个定时炸弹,因为你无法唯一的确定到底是什么原因导致的返回值为 False(可能是数据比较、也可能是数据类型不一致)。
P3 则对其进行了修正,如果比较操作数类型不一致时,会触发 TypeError 异常。
兼容技巧:永远不要比较数据类型不一致的对象。
统一抛出异常语法
P2 同时支持新旧两种异常触发语法:
raise IOError, "file error" # Oldraise IOError("file error") # New
P3 则统一使用新异常触发语法,否则会触发 SyntaxError 异常:
raise IOError("file error")
兼容技巧:
### 抛出异常# Python 2 only:raise ValueError, "dodgy value"# Python 2 and 3:raise ValueError("dodgy value")### 使用 traceback 抛出异常# Python 2 only:traceback = sys.exc_info()[2]raise ValueError, "dodgy value", traceback# Python 3 only:raise ValueError("dodgy value").with_traceback()# Python 2 and 3: option 1from six import reraise as raise_# or # from future.utils import raise_traceback = sys.exc_info()[2] raise_(ValueError, "dodgy value", traceback)# Python 2 and 3: option 2from future.utils import raise_with_traceback raise_with_traceback(ValueError("dodgy value"))### 异常链处理# Setup:class DatabaseError(Exception): pass# Python 3 onlyclass FileDatabase: def __init__(self, filename): try: self.file = open(filename) except IOError as exc: raise DatabaseError('failed to open') from exc# Python 2 and 3:from future.utils import raise_fromclass FileDatabase: def __init__(self, filename): try: self.file = open(filename) except IOError as exc: raise_from(DatabaseError('failed to open'), exc)
统一异常处理语法
P2 实现异常处理也能够支持两种语法。
try: let_us_cause_a_NameErrorexcept NameError, err:# except NameError as err: print err, '--> our error message'
P3 的异常处理则强制要求使用 as 关键字的方式。
try: let_us_cause_a_NameErrorexcept NameError as err: print(err, '--> our error message')
兼容技巧:统一使用 as 关键字的异常处理方式。
统一输入函数
P2 支持 raw_input 和 input 两个输入函数,区别在于前者仅能返回 String 类型对象,后者则支持返回数字和字符串两种数据类型对象,并且当输入为表达式时,会隐式调用 eval 函数返回其执行结果。显然的,使用 input 是更加灵活的写法。
所以 P3 统一的使用了 input 函数进行输入处理。
兼容技巧:统一使用 input 内置函数。
# Python 2 only:input("Type something safe please: ")# Python 2 and 3from future.builtins import input eval(input("Type something safe please: "))
统一输出函数
P2 中的 print 即是关键字又是内置函数。print 'Hello world!'
为一条语句,print('Hello world!')
则为一次函数调用。
P3 统一使用 print 函数进行输出操作,其原型如下,这一改变让 P3 的输出处理变得更加简洁、强大而优雅,通过实参的传递就能替代 P2 中繁复的代码实现。
print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
兼容技巧:
### 单行打印单个 String# Python 2 only:print 'Hello'# Python 2 only:print 'Hello'### 单行打印多个 String# Python 2 only:print 'Hello', 'Guido'# Python 2 and 3:from __future__ import print_function # (at top of module)print('Hello', 'Guido')### 输出重定向# Python 2 only:print >> sys.stderr, 'Hello'# Python 2 and 3:from __future__ import print_function print('Hello', file=sys.stderr)### 换行打印# Python 2 only:print 'Hello',# Python 2 and 3:from __future__ import print_function print('Hello', end='')
统一文件操作函数
P2 支持使用 file 和 open 两个函数来进行文件操作。
P3 则统一使用 open 来进行文件操作。
兼容技巧:统一使用 open 函数。
# Python 2 only:f = file(pathname)# Python 2 and 3:f = open(pathname)
统一列表迭代器生成函数
P2 支持使用 range 和 xrange 两个函数来生成可迭代对象,区别在于前者返回的是一个列表类型对象,后者返回的是一个类似生成器(惰性求值)的迭代对象,支持无限迭代。所以当你需要生成一个很大的序列时,推荐使用 xrange,因为它不会一上来就索取序列所需的所有内存空间。如果只对序列进行读操作的话,xrange 方法效率显然会更高,但是如果要修改序列的元素,或者往序列增删元素的话,那只能通过 range 方法生成一个 list 对象了。
P3 则统一使用 range 函数来生成可迭代对象,但其实 P3 的 range 更像是 P2 的 xrange。所以在 P3 中如果你想得到一个可以被修改的列表对象,你需要这么做:
list(range(1,10))[1, 2, 3, 4, 5, 6, 7, 8, 9]
兼容技巧:统一使用 range 函数
# Python 2 only:for i in xrange(10**8): ...# Python 2 and 3: forward-compatiblefrom future.builtins import rangefor i in range(10**8): ...# Python 2 and 3: backward-compatiblefrom past.builtins import xrangefor i in xrange(10**8): ...
统一迭代器迭代函数
P2 中支持使用内置函数 next 和迭代器对象的 .next() 实例方法这两种方式来获取迭代器对象的下一个元素。所以,在实现自定义迭代器对象类时,必须实现 .next() 实例方法:
# Python 2 onlyclass Upper(object): def __init__(self, iterable): self._iter = iter(iterable) def next(self): # Py2-styface iterator interface return self._iter.next().upper() def __iter__(self): return self itr = Upper('hello')assert itr.next() == 'H' # Py2-styleassert list(itr) == list('ELLO')
但在 P3 中统一了使用 next 内置函数来获取下一个元素,如果试图调用 .next() 方法则会触发 AttributeError 异常。所以,在 P3 中实现自定义迭代器所要实现的是 __next__
特殊方法。
兼容技巧:
# Python 2 and 3: option 1from future.builtins import objectclass Upper(object): def __init__(self, iterable): self._iter = iter(iterable) def __next__(self): # Py3-style iterator interface return next(self._iter).upper() # builtin next() function calls def __iter__(self): return self itr = Upper('hello')assert next(itr) == 'H' # compatible styleassert list(itr) == list('ELLO')# Python 2 and 3: option 2from future.utils import implements_iterator@implements_iteratorclass Upper(object): def __init__(self, iterable): self._iter = iter(iterable) def __next__(self): # Py3-style iterator interface return next(self._iter).upper() # builtin next() function calls def __iter__(self): return self itr = Upper('hello')assert next(itr) == 'H'assert list(itr) == list('ELLO')