经历移植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有很多交叉哦语法和特性,代码可以兼容这两个版本。
- 字符串兼容。2.6和3.3支持相同的字符串语法。你可以用 "foo" 表示原生字符串(2.x表示byte,3.x表示unicode),u"foo" 表示unicode字符串,b"foo" 表示原生字符串或字节数组。
- print函数兼容,如果你的print语句比较少,那你可以加上"from __future__ import print_function",然后开始使用print函数,而不是把它绑定到别的变量上,进而避免诡异的麻烦。
- 兼容的异常语法。Python 2.6引入的 "except Exception as e" 语法也是3.x的异常捕捉语法。
- 类修饰器都有效。这个可以用在修改接口而不在类结构定义中留下痕迹。例如可以修改迭代方法名字,也就是把 next 改成 __next__ 或者把 __str__ 改成 __unicode__ 来兼容python 2.x。
- 内置next调用__next__或next。这点很有用,因为他们和直接调用方法的速度差不多,所以你不用考虑得太多而去加入运行时检查和包装一个函数。
- Python 2.6 加入了和python 3.3接口一样的bytearray类型。这点也很有用,因为2.6没有 3.3的byteobject类型,虽然有一个内建的名字但那仅仅只是str的别名,并且使用习惯也有很大差异。
- Python 3.3又加入了byte到byte和string到string的编码与解码,这已经在3.1和3.2中去掉了,很不幸,他们的接口很复杂了,别名也没了,但至少更比以前的2.x版本更接近了。
最后一点在流编码和解码的时候很有用,这功能在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,你能够完全抛离帮助文档么?答案当然是否定的。你依然需要一个小的兼容模块,但是它足够小,使得你能够将它仅仅放在你的包中,下面是一个基本的例子,关于一个兼容模块看起来是个什么样子:
import sys PY2 = sys.version_info[0] == 2 if not PY2: text_type = str string_types = (str,) unichr = chr else: text_type = unicode string_types = (str, unicode) unichr = unichr</div>
那个模块确切的内容依赖于,对于你有多少实际的改变。在Jinja2中,我在这里放了一堆的函数。它包括ifilter, imap以及类似itertools的函数,这些函数都内置在3.x中。(我纠缠Python 2.x函数,是为了让读者能够对代码更清楚,迭代器行为是内置的而不是缺陷) 。
为2.x版本做测试而不是3.x
总体上来说你现在正在使用的python是2.x版本的还是3.x版本的是需要检查的。在这种情况下我推荐你检查当前版本是否是python2而把python3放到另外一个判断的分支里。这样等python4面世的时候你收到的“惊喜”对你的影响会小一点
好的处理:
if PY2:
def __str__(self):
return self.__unicode__().encode('utf-8')
</div>
相比之下差强人意的处理:
if not PY3:
def __str__(self):
return self.__unicode__().encode('utf-8')
</div>
字符串处理
Python 3的最大变化毫无疑问是对Unicode接口的更改。不幸的是,这些更改在某些地方非常的痛苦,而且在整个标准库中还得到了不一致地处理。大多数与字符串处理相关的时间函数的移植将完全被废止。字符串处理这个主题本身就可以写成完整的文档,不过这儿有移植Jinja2和Werkzeug所遵循的简洁小抄:
'foo'这种形式的字符串总指的是本机字符串。这种字符串可以用在标识符里、源代码里、文件名里和其他底层的函数里。另外,在2.x里,只要限制这种字符串仅仅可使用ASCII字符,那么就允许作为Unicode字符串常量。
这个属性对统一编码基础是非常有用的,因为Python 3的正常方向时把Unicode引进到以前不支持Unicode的某些接口,不过反过来却从不是这样的。由于这种字符串常量“升级”为Unicode,而2.x仍然在某种程度上支持Unicode,因此这种字符串常量怎么用都行。
例如 datetime.strftime函数在Python2里严格不支持Unicode,并且只在3.x里支持Unicode。不过因为大多数情况下2.x上的返回值只是ASCII编码,所以像这样的函数在2.x和3.x上都确实运行良好。
>>> u'<p>Current time: %s' % datetime.datetime.utcnow().strftime('%H:%M')
u'<p>Current time: 23:52'
</div>
传递给strftime的字符串是本机字符串(在2.x里是字节,而在3.0里是Unicode)。返回值也是本机字符串并且仅仅是ASCII编码字符。 因此在2.x和3.x上一旦对字符串进行格式化,那么结果就一定是Unicode字符串。
u'foo'这种形式的字符串总指的是Unicode字符串,2.x的许多库都已经有非常好的支持Unicode,因此这样的字符串常量对许多人来说都不应该感到奇怪。
b'foo'这种形式的字符串总指的是只以字节形式存储的字符串。由于2.6确实没有类似Python 3.3所具有的字节对象,而且Python 3.3缺乏一个真正的字节字符串,因此这种常量的可用性确实受到小小的限制。当与在2.x和3.x上具有同样接口的字节数组对象绑定在一起时候,它立刻变得更可用了。
由于这种字符串是可以更改的,因此对原始字节的更改是非常有效的,然后你再次通过使用inbytes()封装最终结果,从而转换结果为更易读的字符串。
除了这些基本的规则,我还对上面我的兼容模块添加了 text_type,unichr 和 string_types 等变量。通过这些有了大的变化:
- isinstance(x, basestring) 变成 isinstance(x, string_types).
- isinstance(x, unicode) 变成 isinstance(x, text_type).
- isinstance(x, str) 为表明捕捉字节的意图,现在变成 isinstance(x, bytes) 或者 isinstance(x, (bytes, bytearray)).
我还创建了一个 implements_to_string 装饰类,来帮助实现带有 __unicode__ 或

