python2移植到python3

时间:2015-07-22 19:06:39   收藏:0   阅读:505

移植到 Python 3

经历移植jinja2到python3的痛苦之后,我把项目暂时放一放,因为我怕打破python3的兼容。我的做法是只用一个python2的代码库, 然后在安装的时候用2to3工具翻译成python3。不幸的是哪怕一点点的改动都会打破迭代开发。如果你选对了python的版本,你可以专心做事,幸 运的避免了这个问题。

来自MoinMoin项目的Thomas Waldmann通过我的python-modernize跑jinja2,并且统一了代码库,能同时跑python2,6,2,7和3.3。只需小小清理,我们的代码就很清晰,还能跑在所有的python版本上,并且看起来和普通的python代码并无区别。

受到他的启发,我一遍又一遍的阅读代码,并开始合并其他代码来享受统一的代码库带给我的快感。

下面我分享一些小窍门,可以达到和我类似的体验。

放弃python 2.5 3.1和3.2

这是最重要的一点,放弃2.5比较容易,因为现在基本没人用了,放弃3.1和3.2也没太大问题,应为目前python3用的人实在是少得可怜。但是你为什么放弃这几个版本呢?答案就是2.6和3.3有很多交叉哦语法和特性,代码可以兼容这两个版本。

最后一点在流编码和解码的时候很有用,这功能在3.0的时候去掉了,直到3.3才恢复。

没错,six模块可以让你走得远一点,但是不要低估了代码工整度的意义。在Python3移植过程中,我几乎对jinja2失去了兴趣,因为代码开始虐我。就算能统一代码库,但还是看起来很不舒服,影响视觉(six.b(‘foo‘)和six.u(‘foo‘)到处飞)还会因为用2to3迭代开发带来不必要的麻烦。不用去处理这些麻烦,回到编码的快乐享受中吧。jinja2现在的代码非常清晰,你也不用当心python2和3的兼容问题,不过还是有一些地方使用了这样的语句:if PY2:。

接下来假设这些就是你想支持的python版本,试图支持python2.5,这是一个痛苦的事情,我强烈建议你放弃吧。支持3.2还有一点点可能,如果你能在把函数调用时把字符串都包装起来,考虑到审美和性能,我不推荐这么做。

跳过six

six是个好东西,jinja2开始也在用,不过最后却不给力了,因为移植到python3的确需要它,但还是有一些特性丢失了。你的确需要six,如果 你想同时支持python2.5,但从2.6开始就没必要使用six了,jinja2搞了一个包含助手的兼容模块。包括很少的非python3 代码,整个兼容模块不足80行。

因为其他库或者项目依赖库的原因,用户希望你能支持不同版本,这是six的确能为你省去很多麻烦。

开始使用Modernize

使用python-modernize移植python是个很好的还头,他像2to3一样运行的时候生成代码。当然,他还有很多bug,默认选项也不是很合理,可以避免一些烦人的事情,然你走的更远。但是你也需要检查一下结果,去掉一些import 语句和不和谐的东西。

修复测试

做其他事之前先跑一下测试,保证测试还能通过。python3.0和3.1的标准库就有很多问题是诡异的测试习惯改变引起的。

写一个兼容的模块

因此你将打算跳过six,你能够完全抛离帮助文档么?答案当然是否定的。你依然需要一个小的兼容模块,但是它足够小,使得你能够将它仅仅放在你的包中,下面是一个基本的例子,关于一个兼容模块看起来是个什么样子:

01 import sys
02 PY2 = sys.version_info[0] == 2
03 if not PY2:
04     text_type = str
05     string_types = (str,)
06     unichr = chr
07 else:
08     text_type = unicode
09     string_types = (str, unicode)
10     unichr = unichr

那个模块确切的内容依赖于,对于你有多少实际的改变。在Jinja2中,我在这里放了一堆的函数。它包括ifilterimap以及类似itertools的函数,这些函数都内置在3.x中。(我纠缠Python 2.x函数,是为了让读者能够对代码更清楚,迭代器行为是内置的而不是缺陷) 。

为2.x版本做测试而不是3.x

总体上来说你现在正在使用的python是2.x版本的还是3.x版本的是需要检查的。在这种情况下我推荐你检查当前版本是否是python2而把python3放到另外一个判断的分支里。这样等python4面世的时候你收到的“惊喜”对你的影响会小一点

好的处理:

1 if PY2:
2     def __str__(self):
3         return self.__unicode__().encode(‘utf-8‘)

相比之下差强人意的处理:

1 if not PY3:
2     def __str__(self):
3         return self.__unicode__().encode(‘utf-8‘)

字符串处理

Python 3的最大变化毫无疑问是对Unicode接口的更改。不幸的是,这些更改在某些地方非常的痛苦,而且在整个标准库中还得到了不一致地处理。大多数与字符串 处理相关的时间函数的移植将完全被废止。字符串处理这个主题本身就可以写成完整的文档,不过这儿有移植Jinja2和Werkzeug所遵循的简洁小抄:

除了这些基本的规则,我还对上面我的兼容模块添加了 text_type,unichr 和 string_types 等变量。通过这些有了大的变化:

我还创建了一个 implements_to_string 装饰类,来帮助实现带有 __unicode__ 或 __str__ 的方法的类:

1 if PY2:
2     def implements_to_string(cls):
3         cls.__unicode__ = cls.__str__
4         cls.__str__ = lambda x: x.__unicode__().encode(‘utf-8‘)
5         return cls
6 else:
7     implements_to_string = lambda x: x

这个想法是,你只要按2.x和3.x的方式实现 __str__,让它返回Unicode字符串(是的,在2.x里看起来有点奇怪),装饰类在2.x里会自动把它重命名为 __unicode__,然后添加新的 __str__ 来调用 __unicode__ 并把其返回值用 UTF-8 编码再返回。在过去,这种模式在2.x的模块中已经相当普遍。例如 Jinja2 和 Django 中都这样用。

下面是一个这种用法的实例:

1 @implements_to_string
2 class User(object):
3     def __init__(self, username):
4         self.username = username
5     def __str__(self):
6         return self.username

元类语法的更改

由于Python 3更改了定义元类的语法,并且以一种不兼容的方式调用元类,所以这使移植比未更改时稍稍难了些。Six有一个with_metaclass函数可以解决这 个问题,不过它在继承树中产生了一个虚拟类。对Jinjia2移植来说,这个解决方案令我非常 的不舒服,我稍稍地对它进行了修改。这样对外的API是相同的,只是这种方法使用临时类与元类相连接。 好处是你使用它时不必担心性能会受影响并且让你的继承树保持得很完美。
这样的代码理解起来有一点难。 基本的理念是利用这种想法:元类可以自定义类的创建并且可由其父类选择。这个特殊的解决方法是用元类在创建子类的过程中从继承树中删除自己的父类。最终的 结果是这个函数创建了带有虚拟元类的虚拟类。一旦完成创建虚拟子类,就可以使用虚拟元类了,并且这个虚拟元类必须有从原始父类和真正存在的元类创建新类的 构造方法。这样的话,既是虚拟类又是虚拟元类的类从不会出现。
这种解决方法看起来如下:

1 def with_metaclass(meta, *bases):
2     class metaclass(meta):
3         __call__ = type.__call__
4         __init__ = type.__init__
5         def __new__(cls, name, this_bases, d):
6             if this_bases is None:
7                 return type.__new__(cls, name, (), d)
8             return meta(name, bases, d)
9     return metaclass(‘temporary_class‘, None, {})

下面是你如何使用它:

1 class BaseForm(object):
2     pass
3
4 class FormType(type):
5     pass
6
7 class Form(with_metaclass(FormType, BaseForm)):
8     pass

字典

Python 3里更令人懊恼的更改之一就是对字典迭代协议的更改。Python2里所有的字典都具有返回列表的keys()、values()和items(),以及 返回迭代器的iterkeys(),itervalues()和iteritems()。在Python3里,上面的任何一个方法都不存在了。相反,这些 方法都用返回视图对象的新方法取代了。

keys()返回键视图,它的行为类似于某种只读集合,values()返回只读容器并且可迭代(不是一个迭代器!),而items()返回某种只读的类集合对象。然而不像普通的集合,它还可以指向易更改的对象,这种情况下,某些方法在运行时就会遇到失败。

站在积极的一方面来看,由于许多人没有理解视图不是迭代器,所以在许多情况下,你只要忽略这些就可以了。

Werkzeug和Dijango实现了大量自定义的字典对象,并且在这两种情况下,做出的决定仅仅是忽略视图对象的存在,然后让keys()及其友元返回迭代器。 由于Python解释器的限制,这就是目前可做的唯一合理的事情了。不过存在几个问题:

下面是Jinja2编码库常具有的对字典进行迭代的情形:

1 if PY2:
2     iterkeys = lambda d: d.iterkeys()
3     itervalues = lambda d: d.itervalues()
4     iteritems = lambda d: d.iteritems()
5 else:
6     iterkeys = lambda d: iter(d.keys())
7     itervalues = lambda d: iter(d.values())
8     iteritems = lambda d: iter(d.items())

为了实现类似对象的字典,类修饰符再次成为可行的方法:

01 if PY2:
02     def implements_dict_iteration(cls):
03         cls.iterkeys = cls.keys
04         cls.itervalues = cls.values
05         cls.iteritems = cls.items
06         cls.keys = lambda x: list(x.iterkeys())
07         cls.values = lambda x: list(x.itervalues())
08         cls.items = lambda x: list(x.iteritems())
09         return cls
10 else:
11     implements_dict_iteration = lambda x: x

在这种情况下,你需要做的一切就是把keys()和友元方法实现为迭代器,然后剩余的会自动进行:

01 @implements_dict_iteration
02 class MyDict(object):
03     ...
04
05     def keys(self):
06         for key, value in iteritems(self):
07             yield key
08
09     def values(self):
10         for key, value in iteritems(self):
11             yield value
12
13     def items(self):
14         ...

通用迭代器的更改

由于一般性地更改了迭代器,所以需要一丁点的帮助就可以使这种更改毫无痛苦可言。真正唯一的更改是从next()到__next__的转换。幸运的是这个 更改已经经过透明化处理。 你唯一真正需要更改的事情是从x.next()到next(x)的更改,而且剩余的事情由语言来完成。

如果你计划定义迭代器,那么类修饰符再次成为可行的方法了:

1 if PY2:
2     def implements_iterator(cls):
3         cls.next = cls.__next__
4         del cls.__next__
5         return cls
6 else:
7     implements_iterator = lambda x: x

为了实现这样的类,只要在所有的版本里定义迭代步长方法__next__就可以了:

1 @implements_iterator
2 class UppercasingIterator(object):
3     def __init__(self, iterable):
4         self._iter = iter(iterable)
5     def __iter__(self):
6         return self
7     def __next__(self):
8         return next(self._iter).upper()

转换编解码器

Python 2编码协议的优良特性之一就是它不依赖于类型。 如果你愿意把csv文件转换为numpy数组的话,那么你可以注册一个这样的编码器。然而自从编码器的主要公共接口与字符串对象紧密关联后,这个特性不再 为众人所知。由于在3.x里转换的编解码器变得更为严格,所以许多这样的功能都被删除了,不过后来由于证明转换编解码有用,在3.3里重新引入了。基本上 来说,所有Unicode到字节的转换或者相反的转换的编解码器在3.3之前都不可用。hex和base64编码就位列与这些编解码的之中。

下面是使用这些编码器的两个例子:一个是字符串上的操作,一个是基于流的操作。前者就是2.x里众所周知的str.encode(),不过,如果你想同时支持2.x和3.x,那么由于更改了字符串API,现在看起来就有些不同了:

1 >>> import codecs
2 >>> codecs.encode(b‘Hey!‘, ‘base64_codec‘)
3 ‘SGV5IQ==\n‘

同样,你将注意到在3.3里,编码器不理解别名,要求你书写编码别名为"base64_codec"而不是"base64"。

(我们优先选择这些编解码器而不是选择binascii模块里的函数,因为通过对这些编码器增加编码和解码,就可以支持所增加的编码基于流的操作。)

其他注意事项

仍然有几个地方我尚未有良好的解决方案,或者说处理这些地方常常令人懊恼,不过这样的地方会越来越少。不幸是的这些地方的某些现在已经是Python 3 API的一部分,并且很难被发现,直到你触发一个边缘情形的时候才能发现它。

展望

统一2.x和3.x的基本编码库现在确实可以开始了。移植的大量时间仍然将花费在试图解决有关Unicode以及与其他可能已经更改了自身API的模块交互时API是如何操作上。无论如何,如果你打算考虑移植库的话,那么请不要触碰2.5以下的版本、3.0-3.2版本,这样的话将不会对版本造成太大的伤害

原文地址:http://lucumr.pocoo.org/2013/5/21/porting-to-python-3-redux/


原文:http://my.oschina.net/bruceray/blog/482423

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!