Supporting Python 3——不使用2to3转换支持Python 2和Python 3

        

不使用2to3转换支持Python 2和Python 3

虽然Python 3的官方文档努阴人们写同时支持Python 2和Python 3的代码,但是在一此情况这是合适的。尤其是你不能放弃支持Python 2.5及更早的版本时,因为Python 2.6引进了相当多的向前兼容。

使相同的代码在更新的版本同样运行是有可能的,不过你开始进入Python 3文档中提到的“扭曲”的代码风格。我会使用一些技巧来做这些并且我在本章末尾提到的six模块会提供很多帮助。它甚至在一些比较大的项目中使用,但是我通常不推荐在在型项目中使用。对于小项目或者大项目的一部分,例如引导脚本,不使用2to3来支持老的Python版本是最好的解决的方式。

Python 2.7对Python 3的兼容有一些小改进,但如果你想要在Python 2和Python 3下执行相同的代码,似乎你必须要在将来的一段时间内支持Python 2.6。

很多你需要的修改将会被2to3处理,所以开始转换你的代码你事实上需要首先在你的代码上运行2to3并且确保你的代码可以在Python 3下运行。它通常很容易,或者至少在Python 3代码中引入Python 2兼容比在Python 2中引入Python 3代码更不单调。

一旦你有一个在Python 3下的项目,偿试在Python 2.6下运行他们。在这一步你可以执行出语法错误。他们应该只是来由print语句的变化。一旦你修复了他们,你可以修复剩下的错误,然后你最终可以在更早的Python版本做相同的事,如果你想同样支持他们。

支持print()函数

语法错误的较多情况是print从语句变成了一个函数。这个简单的情况不是问题,你可以简单地在需要打印的文字周围加上括号。后面的例子能够在所有的Python版本正确地打印出相同的内容:

>>> print("This works in all versions of Python!")
This works in all versions of Python!

然而,如果你使用了一些更高级的print特性However, 要么你会以一个语法错误结束,要么打印出的不是你希望的。Python 2的后续命令在Python 3中变成了一个参数,所以如果你使用后续命令来取消打印之后的新行,在Python 3可以这样做print(‘Textto print’, end=’ ‘),而这在Python 2是一个语法错误。

在Python 2.6下有一个__future__导入可以让print成为一个函数。所以为了避免一些语法错误及其他一些不同,在你用到print()的文件应该要以from __future__ import print_function开始。这个__future__导入只能在Python 2.6及之后的版本工作,所为为了Python 2.5和更早的版本的你有两个选择。你要么把更复杂的print转换成更简单的,要么使用在Python 2和Python 3下都能工作的特殊print。

写一个你自己的print函数听起来比实际上更复杂。有一个技巧是使用sys.stdout.write()并且依照在函数中传入的参数做格式化。然而更加容易使用的是我将要本章结尾提到的six模块中的print_()函数。

处理异常

如果你在你的异常处理里需要访问异常本身,你需要使用一个异常变量。在Python 2 这个语法是:

>>> try:
...     a = 1/0
... except ZeroDivisionError, e:
...     print e.args[0]
integer division or modulo by zero

然而,当你要捕捉不只一个异常时这个语法是令人困惑的。你需要在异常值班表周围加上括号:

>>> try:
...     a = 1/"Dinsdale"
... except (ZeroDivisionError, TypeError):
...     print "You can't divide by zero or by strings!"
You can't divide by zero or by strings!

如果你忘记了括号,那么只有ZeroDivisionError会被捕捉并且引起的异常储存在一个名为TypeError的变量里。那不是你期望有。因此,在Python 3,这个语法改成使用as来取代逗号,以避免错误。当你捕捉多个异常时如果没有括号它也会给你一个语法错误:

>>> try:
...     a = 1/'0'
... except (ZeroDivisionError, TypeError) as e:
...     print(e.args[0])
unsupported operand type(s) for /: 'int' and 'str'

前面的语法也可以在Python 2.6中工作,Python 2.6可以同时使用旧的逗号语法和旧的as关键字语法。如果你需要支持比Python 2.6更低的Python版本并且你需要访问产生的异常,你可以通过exc_info()函数在所有版本做到:

>>> import sys
>>> try:
...     a = 1/'0'
... except (ZeroDivisionError, TypeError):
...     e = sys.exc_info()[1]
...     print(e.args[0])
unsupported operand type(s) for /: 'int' and 'str'

另一个不同之处在于Exception是不长的迭代器。在Python 2给异常的参数是可以迭代遍历异常或者下标异常。

>>> try:
...    f = 1/0
... except ZeroDivisionError, e:
...    for m in e:
...        print m
...    print e[0]
integer division or modulo by zero
integer division or modulo by zero

在Python 3你需要使用异常的args属性来替代:

>>> try:
...    f = "1" + 1
... except TypeError as e:
...    for m in e.args:
...        print(m)
...    print(e.args[0])
Can't convert 'int' object to str implicitly
Can't convert 'int' object to str implicitly

一个message属性已经被添加到了Python 2.5的内置异常。然而它在Python 2.6已经被弃用并在Python 3移除。当使用-3标志时Python 2.6和Python 2.7为这个警报你。

导入错误

大改变之一是标准库的整改,结果你在这步得到的常见错误是导入错误。绕过它是很容易的。你可以简单地偿试从Python 3的位置丑话并且在失败的时候从Python 2的位置导入。对于被重命名的模块,你只要把他们导入成新的名字。

>>> try:
...     import configparser
... except ImportError:
...     import ConfigParser as configparser

一些新的模块合并了多个旧的模块,如果你需要的东西是来自多个旧模块那上上面的情况将不能工作。你也可以不用那些有子模块的模块来这些。import httplib as http.client是一个语法错误。urllib和urllib2不仅仅是合并,也整合进了几个子模块。所以你需要分别导入每一个你用到的模块名。这经意味着你同样需要修改你的代码,在Python 2你可以像这样检索一个网页:

>>> import urllib
>>> import urlparse
>>>
>>> url = 'http://docs.python.org/library/'
>>> parts = urlparse.urlparse(url)
>>> parts = parts._replace(path='/3.0'+parts.path)
>>> page = urllib.urlopen(parts.geturl())

在2to3转换之后它看起来是这样:

>>> import urllib.request, urllib.parse, urllib.error
>>> import urllib.parse
>>>
>>> url = 'http://docs.python.org/library/'
>>> parts = urllib.parse.urlparse(url)
>>> parts = parts._replace(path='/3.0'+parts.path)
>>> page = urllib.request.urlopen(parts.geturl())

是的,urllib.parse会被引用两次并且urllib.error虽然被引用但是没有被使用。这就是固定是如何做的,它做了很多额外的努力,所以它导入的比需要的更多。我们需要修复代码使我们直接使用的名字来取代模块的位置,所以能同时在Python 2和Python 3下同时运行的版本最终看起来像这样:

>>> try:
...     from urllib.request import urlopen
...     from urllib.parse import urlparse
... except ImportError:
...     from urlparse import urlparse
...     from urllib import urlopen
...
>>> url = 'http://docs.python.org/library/' 
>>> parts = urlparse(url)
>>> parts = parts._replace(path='/3.0'+parts.path)
>>> page = urlopen(parts.geturl())

整数不兼容

在Python 3的整数处理上有两个大变化。第一个是int和long类型被合并。这意味着你不能再通过添加L结尾来指定整数是long类型。1L在Python 3是一个语法错误。

如果你一定要在Python 2中使用长整形整数并且兼容Python 3,下面的代码可以做到。它在Python 3定义了一个跟int类一样的long变量,并且它之后可以被用于明显地确保整数是一个长整形。

>>> import sys
>>> if sys.version_info > (3,):
...     long = int
>>> long(1)
1L

另一个变化是八进制字面量语法的变化。在Python 2一个0开始的数字是八进制,其中如果你打算用零来开始一个数字时会产生混淆。在Python 3这个语法换成了更不混淆的0o,相似的0x用于十六进制数字。用0开始一个数字是一个语法错误,这防止了你错误地使用旧式八进制语法。

八进制几乎只用于Unix下的权限设置,但这又是一个相当常见的任务。有一个能同时在Python 2和Python 3工作的简单方式:使用十进制或者十六进制值并把这个八进制值放进一个命令里:

>>> f = 420 # 在八进制是644, 'rw-r--r--'

更多的字节、字符串和Unicode

我们在不用2to3同时支持Python 2和Python 3时遇到的最棘手问题是处理二进制数据,这一点也不奇怪,就像使用2to3时一样。当你们选择不用2to3时这个问题通Unicode问题产生了更多了麻烦,当使用2to3来支持Python 2和Python 3时2to3会把Unicode文字转换成直接的字符串文字。不使用2to3我就没有这种愉快并且因为Un文字u”在Python 3已经消失我们需要找到一种方式来说我们想要一种能在所有Python版本下工作的Unicode字符串。

这里,只支持Python 3.3可以让事情更容易很多,因为在Python 3.3u”文字回来了。在这种情况下,你几乎可以乎略这部分。

但是如果你需要支持Python 3.1或者3.2,要做这个的最好方式是写一个像在常见迁移问题中的b()函数那样只是使用Unicode字符串代替二进制字节的Unicode字符串产生函数。给这个函数的天然名字当然是u()。然后我们使用b()来代替b”文字,并且使用u()来取代u”文字。

import sys
if sys.version_info < (3,):
    import codecs
    def u(x):
        return codecs.unicode_escape_decode(x)[0]
else:
    def u(x):
        return x

这将会在Python 2中返回一个unicode对象:

>>> from makeunicode import u
>>> u('GIF89a')
u'GIF89a'

但是它在Python 3返回一个字符串对象:

>>> from makeunicode import u
>>> u('GIF89a')
'GIF89a'

这里我使用unicode_escape编码,因为如果你用和函数指定编码不同的编码来保存文件时其它的编码会失败。unicode_escape在输入字符和保存文件上多做了一些工作,但是它会在不同的Python版本以及不同的操作系统平台上工作。

unicode_escape编码会转换输入unicode字符的所有版本的方式。’\x00′ 语法,’\u0000′ 甚至’\N{name}’语法:

>>> from makeunicode import u
>>> print(u('\u00dcnic\u00f6de'))
Ünicöde
>>> print(u('\xdcnic\N{Latin Small Letter O with diaeresis}de'))
Ünicöde

如果你只需要支持Python 2.6或者之后的版本,也可以使用from __future__ import unicode_literals。这可以把文件中的所有字符串文字转成Unicode文字:

>>> from __future__ import unicode_literals
>>> type("A standard string literal")
<type 'unicode'>
>>> type(b"A binary literal")
<type 'str'>

在Python 2下所有带有__future__导入及u()函数的二进制数据类型被叫着str并且文本类型被称为unicode,然而在Python 3下他被称为bytes和str。

绕过这个的最好办法是定义根据不同的Python版本两个变量:text_type 和binary_type,然后再测试一次这些变量。

>>> from __future__ import unicode_literals
>>> import sys
>>> if sys.version_info < (3,):
...     text_type = unicode
...     binary_type = str
... else:
...     text_type = str
...     binary_type = bytes
>>> isinstance('U\xf1ic\xf6de', text_type)
True

对于处理二进制数据你可以使用在《常见的迁移问题》中讨论的相同的技巧。

二乘三是”six(六)“

Python 2和Python 3之间有很多很多更不同寻常并且有时候很微妙的不同。虽然这里提到的技巧也能适合大多数这些不同意,我还是强烈推荐Benjamin Peterson的模块“six”[1]。它包含一个使用于检查Python版本的PY3常量,并且它包含前面提到的b()和u()函数,而且u()函数没有指定一个编码,所以你只能使用ASCII字符。它也包含很多像text_type和 binary_type这样有帮助的常量以及能同时在Python 2和Python 3下同时工作的print_()函数。

它还包含大量重组标准库的的导入,所以取代这章开始中的try/except结构的是你可以用导入six模块中的模块来代替。值得注意的是不支持重组的urllib和urllib2模块,所以你还是需要使用try/except导入技巧。

six模块甚至包含不寻常问题的助理,例如使用已经被改名的元类和函数的属性。虽然它需要Python 2.4及之后的版本,但如果需要的话你可以在更早的Python版本下使用许多它里的技巧。

如果你试图不转换支持Python 2和Python 3,你一定会发现它很有帮助。

附注:

[1]http://pypi.python.org/pypi/six

本文地址:http://my.oschina.net/soarwilldo/blog/522927

在湖闻樟注:

原文http://python3porting.com/noconv.html

引导页Supporting Python 3:(支持Python3):深入指南

目录Supporting Python 3(支持Python 3)——目录

    原文作者:python入门
    原文地址: https://my.oschina.net/soarwilldo/blog/522927
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞