D.8 编写兼容版本2.x和3.x的代码
当我们处于从Python 2转换到Python 3的交叉口时,你可能会想知道否有可能编写无须修改就能同时运行于Python 2和3上的代码。这似乎是一个合理的请求,但是你应该如何开始呢?当用3.x解释器执行Python 2代码时,什么破坏了大部分Python 2代码呢?
D.8.1 对比print和print()
如果你跟我想的一样,那么你将会说前面问题的答案就是print语句。这是开始的一个好地方,所以让我们解决它吧。棘手的部分是,在版本 2.x 中它是一个语句,因此是一个关键字或保留字;而在版本 3.x 中,它只是一个内置函数。换句话说,因为涉及了语言语法,所以你不能使用if语句,而Python还没有#ifdef宏。
让我们尝试把圆括号放在print参数的两侧。
>>> print('Hello World!')
Hello World!
太酷了,这可以同时在Python 2和Python 3上工作了。这就结束了吗?对不起,还没完全结束。
>>> print(10, 20) # Python 2
(10, 20)
这一次你将不会那么幸运,因为前者是一个元组,而在Python 3中,你向print()中传入的是多个参数。
>>> print(10, 20) # Python 3
10 20
如果你思考得再多一点,也许我们可以检查print是否是一个关键字。你可能还记得,有一个包含关键字列表的keyword模块。因为Python 3.x中print不是关键字,所以你可能认为它可以像下面这么简单。
>>> import keyword
>>> 'print' in keyword.kwlist
False
作为一名聪明的程序员,你可能会在2.x版本中尝试它,并期待会返回一个True。尽管你可能是正确的,但你还是会因一个不同的原因而失败。
>>> import keyword
>>> if 'print' in keyword.kwlist:
…from future import print_function
…
File "<stdin>", line 2
SyntaxError: from future imports must occur at the beginning of the file
一种可行的解决方案要求你使用一个具有与 print 功能类似的函数,其中一个就是sys.stdout.write()。另一个解决方案是distutils.log.warn()。不管出于什么原因,我们决定在本书的很多章节中都使用后者。如果你需要无缓冲的输出,那么我认为sys.stderr.write()也可行。
“Hello World !“示例如下所示。
Python 2.x
print 'Hello World!'
Python 3.x
print('Hello World!')
下面这行在两个版本下都能够工作。
Python 2.x & 3.x compatible
from distutils.log import warn as printf printf('Hello World!')
这让我想起了为什么我们没有使用sys.stdout.write(),因为我们将需要在字符串末尾添加一个换行符来匹配行为。
Python 2.x & 3.x compatible
import sys
sys.stdout.write('Hello World!\n')
真正的问题不是这个小干扰,但因为这一点这些函数将不再是print或print()真正的代理。只有当你给出单个代表你输出的字符串时,它们才会工作。任何更复杂的功能都需要你更多的工作量。
D.8.2 将你的方法导入解决方案中
在其他情况下,生活更简单,你可以导入正确的解决方案。在后面的代码中,我们想导入urlopen()函数。在Python 2中,它存在于urllib和urllib2(我们将使用后者)模块中。在Python 3中,它集成到了urllib.request中。在这里,你的适用于版本2.x和3.x的解决方案很整洁和简单。
try:
from urllib2 import urlopen
except ImportError:
from urllib.request import urlopen
考虑到内存保护,也许你会对一个如zip()等知名的内置迭代器(Python 3)版本感兴趣。在Python 2中,迭代器版本是itertools.izip()。Python 3中将这个函数重命名来取代zip()。换句话说,itertools.izip()替换 zip()和它的名字。如果你坚持这个迭代器版本,那么你的导入语句也是相当简单的。
try:
from itertools import izip as zip except ImportError:
pass
一个看起来不美观的例子就是 StringIO 类。在 Python 2 中,纯粹的 Python 版本就是StringIO模块,意味着你通过StringIO.StringIO访问它。考虑到执行速度,还有一个C语言版本,它位于cStringIO.StringIO中。根据Python安装情况,你可能会更加喜欢cStringIO,而如果cStringIO不可用才选择StringIO作为后备。
在Python 3中,Unicode是默认字符串类型,但是如果你做任何类型的网络通信,那么很有可能你必须操作ASCII/bytes字符串。因此相对于StringIO,你更加想要io.BytesIO。为了得到你想要的,下面这种导入方式将有一点丑陋。
try:
from io import BytesIO as StringIO except ImportError:
try:
from cStringIO import StringIO except ImportError:
from StringIO import StringIO
D.8.3 整合在一起
如果你够幸运,这些都将是你需要做出的改变,而剩下的其他代码会比开始时的设置更简单。如果你安装了distutils.log.warn()(类似printf())url .urlopen()、.StringIO的导入,以及正常导入xml.etree.ElementTree(2.5及更新版本),那么利用下面的约8行代码,你就可以编写一个很短的解析器来显示谷歌新闻服务的头条新闻。
g = urlopen('http://news.google.com/news?topic=h&output=rss')
f = StringIO(g.read())
g.close()
tree = xml.etree.ElementTree.parse(f)
f.close()
for elmt in tree.getiterator():
if elmt.tag == 'title' and not \
elmt.text.startswith('Top Stories'):
printf('- %s' % elmt.text)
无须修改代码,这个脚本在2.x和3.x版本下运行结果完全相同。当然,如果你使用的是版本2.4及更早版本,那么你需要单独下载ElementTree。
因为本节中的代码片段来自第14章,所以可以查看goognewsrss.py文件来了解实际的完整版。
有些人会觉得这些变化真的开始被坏Python代码的优雅性。毕竟,可读性很重要。如果你喜欢保持代码更整洁,但仍编写无须修改就能运行于版本2.x和3.x上的代码,那么可以查看six包。
six 是一个兼容库,它的主要作用是提供一个接口来保持应用程序代码相同,而从开发者的角度隐藏本节描述的复杂性。要了解关于six的更多信息,请访问http://packages.python.org/six。
不管你是否使用像 six 这样的库还是选择编写自己的代码,我们都希望在这个简短的叙述中显示,完全有可能编写能够运行在2.x和3.x版本中的代码。底线是为了折衷2到3的可移植性,你可能需要牺牲Python的一些优雅性和简洁性。我相信我们会在未来几年内重新考虑这个问题,直到整个世界已经完成了到下一代的过渡。
