和 Python 2.x 说再见!项目移到python3

若是你仍在使用 2.x,那么是时候将你的代码移植到 Python 3 了。html

在技术的长河中,软件、工具、系统等版本的迭代本是常事,但因为使用习惯、版本的兼容性、易用性等因素,不少用户及开发者在使用或作开发的过程当中,并不肯意及时同步更新,而这无疑为 IT 技术圈带来了诸多碎片化问题,正如系统之 Android、Windows,也正如编程语言之 Python。python

近日,Python 由于其版本的碎片化问题遭到了英国国家网络安全中心(NCSC)的点名,NCSC 警告开发者们必须考虑将 Python 2.x 的代码库移植到 Python 3.x 版本上,主要是由于自 2020 年 1 月 1 日起,Python 2.x 将走到其生命的尽头,再也不获得支持(EOL,End-of-life)。与此同时,NCSC 还将继续使用 Python 2.x 的公司比做 EOL 以引诱另外一个 WannaCry(病毒)或 Equifax(信息泄露)事件的发生。git

Python 的应用现状程序员

回望近些年才被 AI 点燃的 Python,其实并不是是一门新的语言,它最先于 1989 年末由知名的荷兰计算机程序员 Guido van Rossum 发明,后来做为一门面向对象、直译式计算机程序设计语言于 1991 年面世。其 30 年的发展历程可谓比编程语言界的常青藤 Java 更为久远。github

而论及 Java,一年两次迭代的速度早已让很多开发者痛苦不堪,其纷纷开启对 Oracle 的吐槽模式,并直言“立刻推出 JDK 13 的你只管更新,不用顾及咱们的感觉,总之咱们还坚守在 JDK 1.x”。事实上,不止 Java,Python 也有着相同的问题,不少人对 Python 旧版本的坚持反而也让该语言的核心开发者们也备受煎熬,由于旧版本在安全性、功能上均没法与新版本相媲美,容易出现不少 Bug。编程

根据著名 IDE 开发商 JetBrains 和 Python 基金会于今年年初发布的《Python Developers Survey 2018 Results》报告显示,Python 3 的采用率正在快速增加,将其做为主要解释器的开发者比例从 2017 年的 75% 上升到了 84%,不过与此同时,Python 2 仍占有 16% 的份额。数组

其中,在 Python 2.x 版本中,Python 2.7 最受欢迎且使用的人数最多,占比 93%。安全

那么这些开发者究竟为什么不肯意升级?网络

Python 的版本之过编程语言

一直以来,语法简单、拥有丰富和强大类库的 Python 被称之为一门胶水语言,它可以很轻松的把用其它语言制做的各类模块(尤为是 C/C++)轻松地联结在一块儿。

不过在版本的迭代过程当中,Python 出现了一个常常被开发者们诟病的问题,即于 2008 年发布的 Python 3 在设计时没有考虑向较早版本相容的问题,Python 2.x 版本与 Python 3.x 之间并不兼容。这意味着若是你的软件是基于 Python 2 开发的,想要迁移到 Python 3 上面,无疑须要耗费巨大的成本。并且在此过程当中,若是项目涉及到诸多关于 Python 2 的类库,可能还会致使迁移失败。

而自此问题的出现让很多本来想要升级的开发者宁愿停留在之前的旧版本中,对此,有很多网友表示:

Python 2.x 和 Python 3.x 二者在编码效率上没有明显差距,可是 Python 3.x 却要花额外的成本处理兼容性问题;感受 Python 2 和 Python 3 是两门不一样的语言,只不过他们的语法类似罢了;......除此以外,根据来自 Python 社区开发和共享软件的存储中心 Python Package Index 统计显示,当前主流的 Python 软件包中仍然有很多使用的是 Python 2.x 版本。且其中,每一个包每月的下载量高达百万次。而想要将这些包移植到 Python 3 上,也绝非是一件易事。

Python 2.x 淘汰乃大势所趋

诚然开发者有多少个不肯意,但 Python 2.x 淘汰已成必然趋势。早在 2018 年 3 月,Python 之父 Guido van Rossum 就曾在邮件列表上宣布 Python 2.7 将于 2020 年 1 月 1 日终止支持,这意味着以后 Python 2 将再也不被统一维护,与之对应的是主流第三方库也不会再提供针对 Python 2 版本的开发支持。不过,想要继续使用旧版本也并不是不可,就如同 Java 同样,交付商业费用便可,但这样的作法在突飞猛进的技术圈中,显然不是长久之计。

现在 NCSC 的警醒,再次告诫开发者们,“若是继续使用不受支持的模块,公司就会冒着组织和数据的安全性风险,由于漏洞早晚会出现,并且没人会修复。”

与此同时,来自 NCSC 的平台安全研究员 Rich M 也于官方博客上列举了不升级 Python 2 将面临的种种问题:

依赖项

许多流行的项目,如 NumPy、Requests 和 TensorFlow 等承诺到 2020 年将中止支持 2.x,而且当前一些项目已经这么作了。

这意味着若是你想使用你喜欢模块的最新功能,那么就须要使用 Python 3。等待更新的时间越长,到时将更改的依赖项的 Python 3 版本会越多,更新起来会变得越困难。

或将阻碍其余开发者

若是你正在维护其余开发者所依赖的库,则可能会阻止他们更新到 Python 3。若是阻碍其余开发者,你会在间接、可能无心中加大其余项目面临的安全风险。

你也许不在公司外部发布任何代码,但要考虑可能也在内部使用你代码的同事。

错失最新的 Python 功能

表达式的收益——容许生成器将其部分操做委托给另外一个生成器。Unicode 字符串——Unicode 处理起来更容易。打印函数——打印函数有额外的功能,使其更灵活。视图和迭代器取代列表——一些众所周知的 API 再也不返回列表。好比说,字典返回键、值或二者的视图。“multi-with”语句——复杂的 with 语句更易于阅读。使用 * 和 ** 解包——扩展 * 可迭代解包运算符和 ** 字典解包运算符的用途。如今能够在函数调用中使用任意数量的解包运算符。纯关键字实参——容许实参出如今 varargs 参数的后面。F 字符串——运行时评估的一种新类型的字符串常量,可能含有任何有效的Python表达式。大量的加速和优化机制。

Python 2.x 如何迁移到 Python 3.x?

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

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

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

下面我分享一些小窍门,能够达到和我相似的体验。

放弃python 2.x和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面世的时候你收到的“惊喜”对你的影响会小一点。

  好的处理方式:

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所遵循的简洁小抄:

  '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

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

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

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

@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是相同的,只是这种方法使用临时类与元类相链接。 好处是你使用它时没必要担忧性能会受影响而且让你的继承树保持得很完美。

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

  这种解决方法看起来以下

 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, {})
10 下面是你如何使用它:
11   
12 class BaseForm(object):
13  pass
14   
15 class FormType(type):
16  pass
17   
18 class Form(with_metaclass(FormType, BaseForm)):
19  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())

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

 1 if PY2:
 2  def implements_dict_iteration(cls):
 3   cls.iterkeys = cls.keys
 4   cls.itervalues = cls.values
 5   cls.iteritems = cls.items
 6   cls.keys = lambda x: list(x.iterkeys())
 7   cls.values = lambda x: list(x.itervalues())
 8   cls.items = lambda x: list(x.iteritems())
 9   return cls
10 else:
11  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)的更改,并且剩余的事情由语言来完成。

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

 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
 8 为了实现这样的类,只要在全部的版本里定义迭代步长方法__next__就能够了:
 9   
10 @implements_iterator
11 class UppercasingIterator(object):
12  def __init__(self, iterable):
13   self._iter = iter(iterable)
14  def __iter__(self):
15   return self
16  def __next__(self):
17   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==\n'

  一样,你将注意到在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块里。

  

1 if PY2:
2  exec('def reraise(tp, value, tb):\n raise tp, value, tb')
3 else:
4  def reraise(tp, value, tb):
5   raise value.with_traceback(tb)

  若是你有部分代码依赖于不一样的语法的话,那么一般来讲前面的exec技巧是很是有用的。不过如今因为exec自己就有不一样的语法,因此你不能用它来执行任何命名空间上的操做。下面给出的代码段不会有大问题,由于把compile用作嵌入函数的eval可运行在两个版本上。另外你能够经过exec自己启动一个exec函数。

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

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

  使用Tox来进行本地测试。可以马上在全部Python版本上运行你的测试是很是有益的,这将为你找到许多问题。

  展望

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

相关文章
相关标签/搜索