经历移植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

那个模块确切的内容依赖于,对于你有多少实际的改变。在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')

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

if not PY3:

def __str__(self):

return self.__unicode__().encode('utf-8')

字符串处理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'

Current time: %s' % datetime.datetime.utcnow().strftime('%H:%M')

u'

Current time: 23:52'

传递给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__ 或 __str__ 的方法的类:

if PY2:

def implements_to_string(cls):

cls.__unicode__ = cls.__str__

cls.__str__ = lambda x: x.__unicode__().encode('utf-8')

return cls

else:

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 中都这样用。

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

@implements_to_string

class User(object):

def __init__(self, username):

self.username = username

def __str__(self):

return self.username

元类语法的更改由于Python 3更改了定义元类的语法,并且以一种不兼容的方式调用元类,所以这使移植比未更改时稍稍难了些。Six有一个with_metaclass函数可以解决这个问题,不过它在继承树中产生了一个虚拟类。对Jinjia2移植来说,这个解决方案令我非常 的不舒服,我稍稍地对它进行了修改。这样对外的API是相同的,只是这种方法使用临时类与元类相连接。 好处是你使用它时不必担心性能会受影响并且让你的继承树保持得很完美。

这样的代码理解起来有一点难。 基本的理念是利用这种想法:元类可以自定义类的创建并且可由其父类选择。这个特殊的解决方法是用元类在创建子类的过程中从继承树中删除自己的父类。最终的结果是这个函数创建了带有虚拟元类的虚拟类。一旦完成创建虚拟子类,就可以使用虚拟元类了,并且这个虚拟元类必须有从原始父类和真正存在的元类创建新类的构造方法。这样的话,既是虚拟类又是虚拟元类的类从不会出现。

这种解决方法看起来如下:

def with_metaclass(meta, *bases):

class metaclass(meta):

__call__ = type.__call__

__init__ = type.__init__

def __new__(cls, name, this_bases, d):

if this_bases is None:

return type.__new__(cls, name, (), d)

return meta(name, bases, d)

return metaclass('temporary_class', None, {})

下面是你如何使用它:

class BaseForm(object):

pass

class FormType(type):

pass

class Form(with_metaclass(FormType, BaseForm)):

pass

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

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

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

Werkzeug和Dijango实现了大量自定义的字典对象,并且在这两种情况下,做出的决定仅仅是忽略视图对象的存在,然后让keys()及其友元返回迭代器。

由于Python解释器的限制,这就是目前可做的唯一合理的事情了。不过存在几个问题:

视图本身不是迭代器这个事实意味着通常状况下你没有充足的理由创建临时对象。

内置字典视图的类集合行为在纯Python里由于解释器的限制不可能得到复制。

3.x视图的实现和2.x迭代器的实现意味着有大量重复的代码。

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

if PY2:

iterkeys = lambda d: d.iterkeys()

itervalues = lambda d: d.itervalues()

iteritems = lambda d: d.iteritems()

else:

iterkeys = lambda d: iter(d.keys())

itervalues = lambda d: iter(d.values())

iteritems = lambda d: iter(d.items())

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

if PY2:

def implements_dict_iteration(cls):

cls.iterkeys = cls.keys

cls.itervalues = cls.values

cls.iteritems = cls.items

cls.keys = lambda x: list(x.iterkeys())

cls.values = lambda x: list(x.itervalues())

cls.items = lambda x: list(x.iteritems())

return cls

else:

implements_dict_iteration = lambda x: x

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

@implements_dict_iteration

class MyDict(object):

...

def keys(self):

for key, value in iteritems(self):

yield key

def values(self):

for key, value in iteritems(self):

yield value

def items(self):

...

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

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

if PY2:

def implements_iterator(cls):

cls.next = cls.__next__

del cls.__next__

return cls

else:

implements_iterator = lambda x: x

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

@implements_iterator

class UppercasingIterator(object):

def __init__(self, iterable):

self._iter = iter(iterable)

def __iter__(self):

return self

def __next__(self):

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,现在看起来就有些不同了:

>>> import codecs

>>> codecs.encode(b'Hey!', 'base64_codec')

'SGV5IQ== '

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

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

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

在Linux上处理文件系统和文件IO访问仍然令人懊恼,因为它不是基于Unicode的。Open()函数和文件系统的层都有危险的平台指定的缺省选项。例如,如果我从一台de_AT的机器SSH到一台en_US机器,那么Python对文件系统和文件操作就喜欢回退到ASCII编码上。

我注意到通常Python3上对文本操作最可靠的同时也在2.x正常工作的方法是仅仅以二进制模式打开文件,然后显式地进行解码。另外,你也可以使用2.x上的codec.open或者io.open函数,以及Python 3上内置的带有编码参数的Open函数。

标准库里的URL不能用Unicode正确地表示,这使得一些URL在3.x里不能被正确的处理。

由于更改了语法,所以追溯对象产生的异常需要辅助函数。通常来说这非常罕见,而且很容易处理。下面是由于更改了语法所遇到的情形之一,在这种情况下,你将不得不把代码移到exec块里。

if PY2:

exec('def reraise(tp, value, tb): raise tp, value, tb')

else:

def reraise(tp, value, tb):

raise value.with_traceback(tb)

如果你有部分代码依赖于不同的语法的话,那么通常来说前面的exec技巧是非常有用的。不过现在由于exec本身就有不同的语法,所以你不能用它来执行任何命名空间上的操作。下面给出的代码段不会有大问题,因为把compile用做嵌入函数的eval可运行在两个版本上。另外你可以通过exec本身启动一个exec函数。

exec_ = lambda s, *a: eval(compile(s, '', 'exec'), *a)

如果你在Python C API上书写了C模块,那么自杀吧。从我知道那刻起到仙子仍然没有工具可处理这个问题,而且许多东西都已经更改了。借此机会放弃你构造模块所使用的这种方法,然后在cffi或者ctypes上重新书写模块。如果这种方法还不行的话,因为你有点顽固,那么只有接受这样的痛苦。也许试着在C预处理器上书写某些令人讨厌的事可以使移植更容易些。

使用Tox来进行本地测试。能够立刻在所有Python版本上运行你的测试是非常有益的,这将为你找到许多问题。

展望

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

python3项目-把项目从Python2.x移植到Python3.x的经验总结相关推荐

  1. python3转码python2_python2写的代码移植到python3后无法转码成中文文本,请大神指教,谢谢~...

    chenmeiying (楼主) 4天前 C:\Users\www-3\AppData\Local\Programs\Python\Python36\python.exe C:/Users/www-3 ...

  2. 实验楼python3中挑战一_实验楼python3学习挑战项目

    实验楼python3学习挑战项目 在实验楼学习python3做的挑战项目做的代码整理. 链接:https://www.shiyanlou.com/courses/596 1.圆的面积 题目: 能够计算 ...

  3. 一起学Python吧~Python3调用Ansible项目实战

    别问我为什么Terminal的命令也要用PyCharm来写,一切都是为了方便面~ #!/bin/env python3 #-*- coding:utf8 -*- #学python3的第十四天(运维开发 ...

  4. python2好还是python3好-新手入门选择Python2还是Python3

    1. 前言 Python的发展很快,几乎每年都在版本迭代.目前Python有两个主要版本,一个是python2.x,另一个是python3.x. 兔子先生最早接触Python的时候,使用的是pytho ...

  5. linux上运行项目,发布项目到 Linux 上运行 Core 项目

    目录索引 简介 ASP.Net Core 给我们带来的最大的亮点就是跨平台,我在我电脑(win7)上用虚拟机建了个 CentOS7 ,来演示下,我们windows上的项目如何发布项目到Linux上运行 ...

  6. 路飞学城项目之项目上线

    文章目录 1.购买服务器 2.连接服务器 3.服务器命令 3.1.管理员权限 3.2.配置终端 3.3.更新系统软件包 3.4.安装软件管理包和可能使用的依赖 3.5.检测是否成功:会将git作为依赖 ...

  7. JZ2440 数码相框项目 扩展项目介绍

    文章目录 链接 背景 项目介绍 1. 环境 2. 注意事项 链接 JZ2440 数码相框项目 扩展项目(一) 多文件图标 (二) 显示png JZ2440 数码相框项目 扩展项目(三) 支持鼠标 JZ ...

  8. 医药领域知识图谱快速及医药问答项目(项目全过程)

    该项目是中科院软件所刘焕勇老师在github上的开源项目,基于知识图谱的医药领域问答项目 Github项目地址:mirrors / liuhuanyong / qasystemonmedicalkg ...

  9. step如何打开服务器项目,STEP7项目打开及删除

    1.请问各位师傅STEP7创建的程序可不可以直接在保存文件的地方打开,而不需要首先启动软件,然后在用户目录中找. --------不可以. 2.打开过多个项目后,用户项目中的内容太多,如何清除去一些已 ...

最新文章

  1. ZooKeeper典型应用场景一览
  2. SpringBoot中整合freemarker时配置文件application.properties示例代码
  3. 日历控件源码开放--适用于ASP.NET 1.1
  4. Oracle数据类型与.NET中的对应关系
  5. Java JSON对象怎么遍历_Java遍历JsonObject对象
  6. (计算机组成原理)第五章中央处理器-第四节1:CPU硬布线控制器的设计原理(逻辑表达式,微操作时序,电路设计)
  7. (46)Xilinx ILA IP核配置(七)(第10天)
  8. 高品质静物空间海报模板PSD分层素材
  9. TensorFlow应用实战-18-Policy Gradient算法
  10. Python安装包下载、环境配置与工具包安装教程(详细版)
  11. 软件的工程化管理(二)(转)
  12. 基于排队论模型的收银台服务系统的分析及可视化设计
  13. JAVASCRIPT加密解密终级指南
  14. 屏幕录制软件有哪些?4款超好用录屏软件下载,强烈推荐
  15. 北京航空航天大学公开课:拓扑学
  16. javascript实现前程无忧的选择城市
  17. 图形界面操作pandas:计算变异系数(极差 四分位差 方差 标准差 协方差 变异系数)
  18. autodesk fbx sdk sample里面的工程无法调试解决方法
  19. hls简述(HTTP live Streaming)
  20. Farmer John的故事——写给程序员们

热门文章

  1. Java实现在线预览功能
  2. Linux(64) 下 Tomcat + java 环境搭建
  3. jmeter响应数据Unicode编码转换为汉字
  4. python 学习总结6 前端学习2
  5. Web/app端自动化测试对比
  6. SQL函数类的操作,增加,查询
  7. Struts2.3.4.1+Spring3.2.3+Hibernate4.1.9整合
  8. 不从事编程、学python有用吗-没想到,学会Python即使不做程序员都能月入过万!...
  9. python详细安装教程3.7.0-Python 3.7.0安装教程(附安装包) | 我爱分享网
  10. python画图三维-Python三维绘图之Matplotlib库的使用方法