Supporting Python 3(支持python3)——为Python 3作准备

为Python3做准备

在开始添加Python 3的支持前,为了可以尽量地顺利过分到Python 3,你应该经过修改对2to3来讲很难苦的东西来给你的代码作一些准备。即便你如今不打算迁移到Python 3,有一些事你也能够如今就作,这些事在一些状况下它们甚至会加快你的代码在Python 2下的运行。html

你可能想要读在用现代的用句来改善你的代码 上包含许多其余一些你可以用到你的代码中的改进的章节。python

在Python 2.7下运行

这个过程的第一步是让你的代码在Python 2.6或者2.7下运行。在这里你用的是什么版本不重要,可是明显最后的Python 2是颇有意义的,因此若是你能用Python 2.7的话,就用吧。算法

大多数代码不用修改就能够直接运行,可是从Python 2.5到2.6有几个变化。在Python 2.6 as 和 with 是关键字,因此若是你使用这些做为变量就须要修改他们。最简单的办法是在变量的尾部加上下划线。安全

>>> with_ = True
>>> as_ = False

你还须要摆脱字符串异常。 使用字串符来抛出异常已经不被推荐很长时间了,主要是由于他们很是不灵活,例如你不能继承他们。app

>>> raise "Something went wrong!"
Traceback (most recent call last):
...
Something went wrong!

在Python 3字符串异常已经彻底消失了。 在Python 2.6中你不能发出他们,可是为了向后兼容你仍是可能捕捉到他们。在一些状况下你须要在你的代码中移除全部字符串异常的使用而且在作任何事以前先让他在Python 2.6下运行。框架

>>> raise Exception("Something went wrong!")
Traceback (most recent call last):
...
Exception: Something went wrong!

下一步是在Python 2.6和Python 2.7下用 -3选项来运行你的代码。这个选项会对那个在Python 3中不支持及2to3不能转换的部分发出警告。它主要针对的是那么已经被弃用好久的和有新替代方式的部分,或者要被从标准库中移除的模块。例如对Classic Mac OS的支持已经被移除了,如今只支持OS X而且由于这个缘由对Classic Mac OS特殊特性的支持模块已经被移除了。函数

你会获得一些下面列出的这么变化以及一些被重组库的警告(但不是所有)。 库的重组变化是很简单的,不须要解释,警告会提醒你新模块名字。工具

当除以整数时用//代替/

是Python 2除两个整数将反回两个整数。那意味着5除以2将返回2。测试

>>> 5/2
2

可是,在Python 3这会返回2.5。大数据

>>> 5/2
2.5

今天在Python 2中的除尘运行符相信整除相除返回的是整数。可是使用2to3自动转换时会不知道操做数的类型是什么 ,因些它不知道除法操做是否是除以整数。 所以它不会在这里作任何转换。这意味着若是你正在使用旧的整数除法操做,你的代码在Python 3下会有不少错误。

因为这个变化从Python 2.2就开始计划了,Python 2.2及以后的版本都包含一个叫着浮点除法的使用两个斜线的新操做。 即便有浮点数,它也老是返回整数。任何你真的想要使用返回完整数字的浮点除法的地方,都应该把除尘操做改为浮点除法操做。

>>> 5//2
2
>>> 5.0//2.0
2.0

一般状况下,Python 2的除法操做是没必要要的。解决这个问题的最多见的方法是把其中一个整数转换成浮点数或者在其中一个数字上添加一个小数点。

>>> 5/2.0
2.5
>>> a = 5
>>> b = 2
>>> float(a)/b
2.5

可是,有一个更合适的办法,那就是容许使用Python 3的行为。这是经过从Python 2.2开始可能使用的__future__ import 作到的。

>>> from __future__ import division
>>> 5/2
2.5

虽然把除号前的一个操做数中转换成浮点数能很好地工做,可是在Python 3中不是没有必要的而且使用__future__ import就能够避免它。

使用带 -3 选项的Python 2.6运行若是你用旧的整数除法的话就会提醒。

使用新式的类

在Python 2有两种类,“旧式”和“新式”。“旧式”的类已经在Python 3中移除了,所以即便没有显示地定义全部的类也都是object(对象)的子类。

在新旧的类上有一些不一样,可是只有不多的状况下他们才会在Python 3下产生问题。若是你使用了多重继承可能会由于不一样解决顺序遇到一些问题。[4]

若是你用了多重继承,所以你应该在添加Python 3支持前切换成新式类。 经过确保全部的对象是object(对象)的子类来作到这个。而且你可能必需要在类定义中改变列出的超类顺序。

单独的二进制数据和字符串

在Python 2,用str对象来保存二进制数据和ASSCII文本,然而保存在unicode的文件数据比能够保存在ASCII的须要更多字符。在Python 3,取代str和unicode对象的是你能够用bytes对象保存二进制数据而且使用str对象保存无论是否是Unicode的全部类型文本数据。即便有明显的差别,Python 3的str类型或多或少相似于Python 2的unicode类型,而bytes类型颇为相似Python 2的str类型。

为这个做准备的第一步是确保没有在二进制和文本数据上使用相同的变量名。在Python 2这不会给你带来麻烦,可是在Python 3会的,所以尽量保证二进制数据和文本是分开的。

在Python 2上,“t”和“b”文件模式标识志能够改变在一些平台(例如Windows)上换行是如何处理的。可是这些标志在Unix上不会产生不一样做用,所以为Unix开发的不少项目每每会乎略那些标志而且在文本模式打开二进制文件。然而在Python 3 这些标志也决定了当你从文件中读数据返回的结果是bytes对象仍是unicode对象。虽然文本标志是默认的,可是必定要添加上它,由于那表示本文模式是有目的的并不仅是你忘记加标志。

带上-3选项执行Python 2.6是不会提醒这个问题的,由于Python 2根本没有办法知道数据是文本仍是二进制数据。

当排序时,使用key来代替cmp

在Python 2带排序方法能够带一个cmp参数,这个参数是一个比较两个值并返回-1,0或者1的函数。

>>> def compare(a, b):
...     """Comparison that ignores the first letter"""
...     return cmp(a[1:], b[1:])
>>> names = ['Adam', 'Donald', 'John']
>>> names.sort(cmp=compare)
>>> names
['Adam', 'John', 'Donald']

从Python 2.4开,.sort()和新的sorted()函数同样(见使用sorted()代替.sort()),sorted()能够带一个返回排序键值的函数做为key参数。

>>> def keyfunction(item):
...     """Key for comparison that ignores the first letter"""
...     return item[1:]
>>> names = ['Adam', 'Donald', 'John']
>>> names.sort(key=keyfunction)
>>> names
['Adam', 'John', 'Donald']

这使用真情更容易而且执行起来更快。当使用cmp参数时,排序比较每一对值,所以对每一个项目都要屡次调用比较函数。一个更大的数据集合将要为每个项目调用更屡次的比较函数。使用key函数,排序使用保存每个项目的键值并比较他们来取代,因些每个项目只调用一次key函数。由于这个缘由比较大数据集合时会快不少。

key函数每每能够用如此简单地用lambda来替代:

>>> names = ['Adam', 'Donald', 'John']
>>> names.sort(key=lambda x: x[1:])
>>> names
['Adam', 'John', 'Donald']

Python 2.4 还引入了reverse参数。

>>> names = ['Adam', 'Donald', 'John']
>>> names.sort(key=lambda x: x[1:], reverse=True)
>>> names
['Donald', 'John', 'Adam']

在你排序的是几个值时,使用key跟使用cmp相较起来差距不是很明显。比方说,咱们想要首先按名字的长度排序而且相同长度的按字母排序。 用key函数作这个并不显而易见,可是解决方法一般先按次要求排序以后再按另外一个要求排序。

>>> names = ['Adam', 'Donald', 'John']
>>> # Alphabetical sort
>>> names.sort()
>>> # Long names should go first
>>> names.sort(key=lambda x: len(x), reverse=True)
>>> names
['Donald', 'Adam', 'John']

这个能够工做是由于从Python 2.3开始采用了timsort排序算法[1]。这是一个平稳的算法,这意味着若是比较的两个项目是相同的它将会保留他们原来的顺序。

你也能够写一个能返回结合了两个键值的值的key函数而且一鼓作气地完成排序。使人惊奇的是这不必定会更快,你须要测试下哪个解决方案在你的状况下会更快,这取决于数据和key函数。

>>> def keyfunction(item):
...     """Sorting on descending length and alphabetically"""
...     return -len(item), item
>>> names = ['Adam', 'Donald', 'John']
>>> names.sort(key=keyfunction)
>>> names
['Donald', 'Adam', 'John']

key参数在Python 2.4引入的,因此若是你想要支持Python 2.3就不能用它了。若是你想要用key函数作不少的排序,最好的办法是为Python 2.3实现一个简单的在Python 2.4及以后版本的sorted()函数而且用那个来替代内置的sorted()。

>>> import sys
>>> if sys.version_info < (2, 4):
...    def sorted(data, key):
...        mapping = {}
...        for x in data:
...            mapping[key(x)] = x
...        keys = mapping.keys()
...        keys.sort()
...        return [mapping[x] for x in keys]
>>> data = ['ant', 'Aardvark', 'banana', 'Dingo']
>>> sorted(data, key=str.lower)
['Aardvark', 'ant', 'banana', 'Dingo']

Python 2.4如今已经有5年那么老了,因此你不太可能须要支持Python 2.3。


警告

使用-3选项运行Python只会在你显示地使用cmp参数时警告你:

>>> l.sort(cmp=cmpfunction)
__main__:1: DeprecationWarning: the cmp argument is not
supported in 3.x

可是若是像下面这样使用将不警告:

>>> l.sort(cmpfunction)

 因此这个语法可能会漏网之鱼。在Python 3运行这些代码时,在这些状况下你会获得一个TypeError: mustuse keyword argument for key function 。


在Python 2.7和Python 3.2及后面的版本有一个函数能够经过一个包装类把比较函数转换成key函数。它是很聪明的,但反而会让比较函数更慢,所以这只是最后的手段。

>>> from functools import cmp_to_key
>>> def compare(a, b): return cmp(a[1:], b[1:])
>>> sorted(['Adam', 'Donald', 'John'], key=cmp_to_key(compare))
['Adam', 'John', 'Donald']

使用丰富的比较运算符

在Python 2最多见的支持对象比较和排序的方式是实现一个使用内置cmp()函数的__cmp__()方法,像这样类就能够根据姓氏排序了:

>>> class Orderable(object):
...
...     def __init__(self, firstname, lastname):
...         self.first = firstname
...         self.last = lastname
...
...     def __cmp__(self, other):
...         return cmp("%s, %s" % (self.last, self.first),
...                    "%s, %s" % (other.last, other.first))
...
...     def __repr__(self):
...         return "%s %s" % (self.first, self.last)
...
>>> sorted([Orderable('Donald', 'Duck'),
...         Orderable('Paul', 'Anka')])
[Paul Anka, Donald Duck]

然则,你能够拥有相似colors这样的即不能比较大小但能够比较是否相等的类,所以从Python 2.1开始也支持一个比较操做符对应一个方法的丰富的比较方法。他们是 __lt__ 对应 <,__le__ 对应<=, __eq__ 对应==, __ne__ 对应!=, __gt__对应> ,以及 __ge__ 对应 >=。

同时拥有丰富的比较方法和__cmp__()方法违反了只有一种方式来实现比较的原则,因此在Python 3对__cmp__()的支持已经被移除了。所以对Python 3,若是你的类须要被比较的话,你必需要全部的丰富比较操做符。 没有必要在开始支持Python 3作这个,但作这些是一种体验。

相对棘手的

写比较方法会至关棘手,由于你可能须要处理不一样类型的比较。若是比较函数不知道如何与其余对象比较时会返回NotImplemented常量。返回的NotImplemented 能够做为一个Python的比较标志来让Python偿试反向比较。因此若是你的__lt__()方法返回NotImplemented那么Python会偿试用调用其余类的__gt__()方法来代替。


注意

这意味着你永远都不该该在你的丰富比较方法中调用其余类的比较操做!你会找到几个转换大于(就像self.__gt__(other))成返回other < self的丰富比较助手的例子。可是你调用other.__lt__(self)却会返回NotImplemented而不是再次偿试self.__gt__(other)而且无限递归。


一旦你理解了全部案例,实现一个好的正解运行的丰富比较操做集不会困难,可是掌握那些却不是彻底不重要。你能够用许多不一样的方式作它,个人首选方式是这样混合,这样就能够同时在Python 2和Python 3很好地工做。

class ComparableMixin(object):
    def _compare(self, other, method):
        try:
            return method(self._cmpkey(), other._cmpkey())
        except (AttributeError, TypeError):
            # _cmpkey not implemented, or return different type,
            # so I can't compare with "other".
            return NotImplemented

    def __lt__(self, other):
        return self._compare(other, lambda s, o: s < o)

    def __le__(self, other):
        return self._compare(other, lambda s, o: s <= o)

    def __eq__(self, other):
        return self._compare(other, lambda s, o: s == o)

    def __ge__(self, other):
        return self._compare(other, lambda s, o: s >= o)

    def __gt__(self, other):
        return self._compare(other, lambda s, o: s > o)

    def __ne__(self, other):
        return self._compare(other, lambda s, o: s != o)

前面提到的Ptyhon 3.2的functools.total_ordering()类装饰器也是一个很好的解决办法,而且它一样能够用复制并用在其它版本上。可是由于它用的类装饰器,因此不能在Python 2.6如下版本使用。

使用前面的混合,你须要实现返回能一反被比较的对象键值的_cmpkey()方法,相似于比较时用的key()函数。实现真情能够相似这样:

>>> from mixin import ComparableMixin
>>> class Orderable(ComparableMixin):
...
...     def __init__(self, firstname, lastname):
...         self.first = firstname
...         self.last = lastname
...
...     def _cmpkey(self):
...         return (self.last, self.first)
...
...     def __repr__(self):
...         return "%s %s" % (self.first, self.last)
...>>> sorted([Orderable('Donald', 'Duck'),
...         Orderable('Paul', 'Anka')])
[Paul Anka, Donald Duck]

若是对象比较时没有实现 _cmpkey()方法或者前面的混合使用self._cmpkey()的返回值返回的值不能被比较,前面的混合将会返回NotImplemented。 这意味着每个对象都要有一个能返回能合其余有能返回一个元组的_cmpkey()的对象比较的_cmpkey()。以及最重要的是如它不能被比较,若是其余对象如何去比较两个对象,运算符会备用地寻问其余对象。这样你就拥有了一个有最大机会进行有意义比较的对象。

实现 __hash__()

在Python 2,若是你想实现 __eq__() 你也须要重写__hash__()。这是由于两个对象比较赶快相等也须要相同的哈希值。 若是对象是可变的,你必需要把设置__hash__成None来把它标定成可变的。这意味着你不能把它用做字典的键值, 这很好,只有不可变对象能够作字典键。

在Python 3,若是你定义了__eq__(), __hash__ 会被自动设置成, 而且对象变成不能被哈希。因此对于Python 3,除非是一个不变的对象或者你想把它做为一个键值,你都不须要重写__hash__()。

被__hash__()返回的值须要是一个整数,两个比较相等的对象必定有相同的哈希值。它必须在对象的整个存活期内保持不变,这也是为何可变对象为何必须设置__hash__ = None来标记它们是不能哈希的。

若是你使用前面说起的实现比较运算符的_cmpkey()方法,那么不实现__hash__()是很容易的:

>>> from mixin import ComparableMixin
>>> class Hashable(ComparableMixin):
...     def __init__(self, firstname, lastname):
...         self._first = firstname
...         self._last = lastname
...
...     def _cmpkey(self):
...         return (self._last, self._first)
...
...     def __repr__(self):
...         return "%s(%r, %r)" % (self.__class__.__name__,
...                                self._first, self._last)
...
...     def __hash__(self):
...         return hash(self._cmpkey())
...
>>> d = {Hashable('Donald', 'Duck'): 'Daisy Duck'}
>>> d
{Hashable('Donald', 'Duck'): 'Daisy Duck'}

这个类属性按照惯例使用前导的下划线来标记成内部使用,但他们不是传统意义上的不可变。若是你想要一个在Python中真正的不可变类,最简单的办法就是继承collections.namedtuple,但那超出了本书的范畴。

确保你没有用任何一个被移除的模块

不少在标准库中的模块在Python 3中已经被丢弃了。他们中的多大数是对不提供被新模块支持的更好接口的旧系统的特别支持。

若是你使用了一些更经常使用的模块,使用-3选项运行Python 2.6 将会警告你。你使用了那些Python 2.6不会发出警告的模块是至关不可能的,可是若是你正在或者计划同时支持Python 2和Python 3,若是有话你必需要尽量替换成如今对应的。

被移除的模块 上有一个被移除模块的列表。

测试覆盖率和tox

有一个好的测试集对任何项目都是有价值的。当你添加Python 3支持时,测试能够把这个过程加快不少,由于你须要一遍又一遍地执行测试而手工测试要花掉大量时间。

用更多的测试来提升测试覆盖率老是一个好主意。最流行的得到你的模块的测试覆盖率的Python 工具是Ned Batchelder的coverage 模块。[2] 许多像zope.testing、nose及py.test这样的测试运行框架都包含对coverage模块的支持,因此你可能已经安装了。

若是你在开发一个支持全部版本Python的模块,为全部这些版本执行测试迅速会变成一个使人厌烦的工做。为了解决这个Holger Krekel制做了一个叫作tox[3]的工具,这个工具会为每个你想要支持的版本安装虚拟环境而且使用一个简单的命令对全部这些版本执行你的测试。它彷佛是一件小事,它是的,可是它会增长一点点更愉快的体验。若是你计划同时支持Python 2和Python 3,你应该试试看。

可选:在字典中使用迭代器方法(iterator-methods)

从Python 2.2开始内置的字典类型有iterkeys()、 itervalues() 、 iteritems() 方法。他们产生的数据像是keys()、values() 和items()产生的,但他们返回的不是列表而是迭代器(iterator),在使用巨大的字典时这能够节省时间和内存。

>>> dict = {'Adam': 'Eve', 'John': 'Yoko', 'Donald': 'Daisy'}
>>> dict.keys()
['Donald', 'John', 'Adam']

>>> dict.iterkeys() 
<dictionary-keyiterator object at 0x...>

在Python 3标准的keys(),、values() 和 items()返回和迭代器很是相似的字典视图。由于不久以后在这些方法中的迭代器变量要被移除。

2to3 会把迭代器方法的使用转换成标准方法。经过明确地使用迭代器方法,你代表不须要一个列表,这对2to3的转换是有帮助的,不然为了安全要用alist(dict.values())来取代dict.values()调用。

Python 2.7也有像.viewitems()、.viewkeys() 和 .viewvalues()这样可用的新视图迭代器,但由于他们在更早的Python版本中不存在因此他们只有在你打算放弃Python 2.6及更早的版本时才有用。

还要注意的是若是你的代码依靠返回的列表,那么你可能误用未知的字典。例如:下面的代码,你事实上不能肯定每次的键的顺序是相同的,由于这个缘由你可能没法预测该代码的行为。这在调试时会带来一些麻烦。

>>> dict = {'Adam': 'Eve', 'John': 'Yoko', 'Donald': 'Daisy'}
>>> dict.keys()
[0]'Donald'

记住,若是你想要遍历字典就使用for x in dict,这样在Python 2和Python 3中都会自动使用迭代器。

>>> dict = {'Adam': 'Eve', 'John': 'Yoko', 'Donald': 'Daisy'}
>>> for x in dict:
...     print '%s + %s == True' % (x, dict[x])
Donald + Daisy == True
John + Yoko == True
Adam + Eve == True

附注

[1] http://en.wikipedia.org/wiki/Timsort
[2] https://pypi.python.org/pypi/coverage
[3] http://testrun.org/tox/latest/
[4] http://www.python.org/download/releases/2.2.3/descrintro/#mro


在湖闻樟注:

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

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

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