【Python专题(二)】Python二三事

Python2 终于在2020年1月1日官方正式退休了,虽然消息大家提前很久就知道了,但是笔者发现周围依然有很多python2的代码并没有迁移到python3上。所以可以预见的是未来一段时间内,我们还是会面临很多python2和python3反复切换的情况。有时候处理这种python版本问题很让人恼火,你清楚问题在哪里,你也知道你要一个一个去找去定位去改,一定能解决问题,但是你懒得这么做,除非明天是deadline。

有没有个办法可以优雅地解决这个问题呢。看完文章就知道啦。

不知道大家有没有注意到,两三年前用python的时候python2和python3简直是势不两立,python3调python2的package很难不报错。但是近两年python3调python2的package几乎不会报错。原因有两个,第一就是早期的很多package本身就是纯python2写的,完全没有做python3的兼容,但是后来的很多package在写的时候就考虑了python2和python3的兼容问题,会分别写一个python2的版本和一个python3的版本。第二个原因就是随着python2和python3兼容性问题日益凸显,很多专门解决兼容性问题的package,诸如future,past,six等,也日渐成熟,这极大的简化了两个版本互相兼容的工作,有时甚至只需要加一行代码就可以让python3支持python2的项目。

01

python2和python3内建函数(builtins)的区别

这种内建函数的区别就是两个版本的原生差别了,上文介绍的 future package 就是专门为了解决这种问题的。

1.print函数

这个应该是大家最熟悉也是最常见的区别。

python2中的print函数是不需要括号的:

print "hello world"

注:现在的python2.7也支持加括号。

python3中的print函数必须加括号:

print("hello world")

所以print这里在python3改到python2的情况下是不需要修改的。

2.除法运算

这个也是比较重要的一个区别。python2中的整数除法默认向下取整,而python3中的整数除法默认返回浮点数。

Python2:

print(4/3)  # output: 1

Python3:

print(4/3)  # output: 1.3333...

3.编码问题

python有2种字符串类型,分别是 strunicode,str类型变量中保存的是ASCII数据,而Unicode类型变量中保存的是Unicode数据。为了便于理解,这里需要展开一下ASCII和Unicode分别是什么。ASCII是一种编码方式,以一个字节(Byte)为单位保存一个字符,共256种状态。对于英文来说,ASCII已经足够使用了,但是考虑到中文、拉丁文等其他语言,256种状态就不够用了,因此就开始用两个字节为单位保存一个字符,这样可以表示65536种字符,足够覆盖世界上所有符号了,由于这种编码统一了世界上所有的符号,因此叫做Unicode编码。除此之外,你可能听过还有一种编码叫做UTF-8,它可以理解为是一种Unicode的优化方案,因为英文并不需要两个字节的Unicode,为了避免内存的浪费,UTF-8会先识别符号类别,根据符号类别决定每个字符读取1个字节还是2个字节的数据。这样,对于每个英文字符,UTF-8就读取1个字节数据,对于中文等其他字符就读取2个字节数据。另外还有一些专门为中文设计的编码例如GB2312,GB18030等,在一些特定情况也会用的到。

说了这么多,python2和python3编码问题到底在哪呢?问题就在于python2和python3在字符串处理的设计思路不同,python2中会默认把所有Unicode读成1个字节然后用ASCII解码,因此默认情况下,ASCII编码的英文字符不会出现任何问题,但是其他字符,例如中文,在读取的时候就会出现 UnicodeDecodeError的错误(相信写过python2的同学一定被这玩意困扰过),过去的解决方案便是在代码第一行加上 # -*- coding: utf-8 -*-让编译器默认使用UTF-8编码。但是python3不会对Unicode做任何解码,保留Unicode字符,然后用默认的解码方式(一般为UTF-8)来解码。

来看个例子:

Python2:

a = "你"  print repr(a)  # 输出: xe4xbdxa0

Python3:

a = "你"  print(repr(a))  # 输出: 你

Python2和python3兼容方案,在代码开头导入:

from __future__ import unicode_literals

这样就会把python2中所有的字符串改成Unicode,而不会默认用ASCII来解码,从而解决python2中的字符串解码问题。

4.引用问题

python2和python3的引用的默认方式也有所不同。python2默认相对路径导入package,而python3默认绝对路径导入package。换言之,python2在import时的默认搜索顺序是:builtin package(python内建库)、当前路径下的库(自己写的文件)、第三方库(安装的第三方库);而python3在import时的默认搜索顺序是:builtin package(python内建库)、第三方库(安装的第三方库)、当前路径下的库(自己写的文件)。这种默认方式的不同也会在项目中导致一些引用问题。

Python2和python3兼容解决方案,让python统一默认绝对路径导入package:

from __future__ import absolute_import

5.迭代器(iterator)区别

python2和python3显式地调用迭代器的方式有所不同。在python2中定义迭代器的方法是类中的 next()方法,但是python3中定义迭代器的方法则是类中的 __next__()方法。python2在调用迭代器输出下一个元素时,是调用对象的 next()方法也就是 obj.next(),而python3在调用迭代器输出下一个元素时,用 next(obj)。这里借用future官网文档中的一个例子来说明:

Python2:

class Upper(object):      def __init__(self, iterable):          self._iter = iter(iterable)      def next(self):          # Py2-style          return self._iter.next().upper()      def __iter__(self):          return self  itr = Upper('hello')  assert itr.next() == 'H'     # Py2-style  assert list(itr) == list('ELLO')

Python3:

class Upper(object):      def __init__(self, iterable):          self._iter = iter(iterable)      def __next__(self):      # Py3-style iterator interface          return next(self._iter).upper()  # builtin next() function calls      def __iter__(self):          return self  itr = Upper('hello')  assert next(itr) == 'H'      # compatible style  assert list(itr) == list('ELLO')

python2和python3兼容方案,在代码前加:

from builtins import object

或者

from future.utils import implements_iterator

6.其他问题

上述的5种兼容性问题只是一部分笔者认为出现频率较高的情况。其实python2和python3之间还有很多细微的不同都可能影响你代码的运行结果和质量,例如字典有序性的改变(python2中的字典是无序的,python3中的字典是有序的)、metaclass的区别、以及map,range等函数的区别等等。由于篇幅和精力有限,本文不再详细探讨,大家可以根据文末的参考文献自行查阅。

02

python2和python3标准库使用的区别

除了一些内建函数的区别,还有很多标准库的使用在python2和python3中略有不同。我这里列举一些我会经常遇到的问题来说明。

1.urllib

urllib是python中使用非常广泛的一个用于网络协议解析,资源请求的标准库,与此同时,它也是最难做到python2和python3兼容的标准库。如果你还没开始写这部分代码,那可以考虑不用这个库, Requests(http://python-requests.org)也许是更好的选择。但是如果你是在修改别人写好的代码,那只能硬着头皮改下去了。这个库在2和3版本里文件结构发生了较大的改变,从引用就可以看出来:

Python2:

from urlparse import urlparse  from urllib import urlencode  from urllib2 import urlopen, Request, HTTPError

Python3:

from urllib.parse import urlparse, urlencode  from urllib.request import urlopen, Request  from urllib.error import HTTPError

Python2和python3兼容的解决方案:

from future.standard_library import install_aliases  install_aliases()  from urllib.parse import urlparse, urlencode  from urllib.request import urlopen, Request  from urllib.error import HTTPError

2.pickle

pickle是用来保存和读取数据结构,文件的标准库。在python2中这个标准库叫做 cPickle:

import cPickle

在python3中这个标准库更名为pickle:

import pickle

Python2和python3解决方案:

import six.moves.cPickle as pickle

3.其他module

本文所列的两个package只是笔者经常会遇到的情况,因此仅对它们做了特别说明。实际上两个版本之间还有很多标准库使用方法不同,由于篇幅和精力的限制,不在此做详细说明,感兴趣可以在文末的参考文献中进一步查阅。

03

Python2/3自动转换

到现在为止,你应该对python2和python3兼容问题有了一个大概的认识了。接下来,我们来具体看看,如何用这些成熟的工具优雅地解决兼容性问题。我们将你可能遇到的场景分为三种,第一,将python3代码改成python2;第二,将python2代码改成python3;第三,自己写的项目同时支持python2和python3。

首先来看场景一:将python3代码改成python2.

这种需求乍看上去很奇怪,但是笔者确实遇到了这种情况。有个package是很早前用python2写的,属于之前我们说的完全没有考虑兼容问题的那一类package,但是这个package又是我做项目必须要用到的,而不幸的是,项目中其他的代码都是用python3写的。因此我必须要在项目中解决兼容问题——把python3的代码改到python2然后用python2运行项目。你可能会问,为什么不把python2的包改成支持python3呢?因为那个package不是我们项目写的,我们不是维护者,改起来可能会出现不可预知的问题,所以最好还是改动自己的代码。解决方案:

pip install future  # 安装future  pasteurize -w mypy3module.py  # 将文件改为可同时支持python2和python3

场景二:将python2代码迁移到python3.

这种需求应该是非常常见的一种了,随着python2彻底成为历史,如果你手上还有不兼容python3的python2代码,确实应该考虑把它迁移到python3了。幸运的是,我们不需要跑去源代码中一一对应python2和python3的区别然后逐一搜索去修改。future提供了一个非常方便的工具—— futurize来帮你实现这个需求:

pip install future  # 安装future  futurize --stage1 -w test/*.py  # 将test路径下的所有.py文件从python2改到python3.  futurize --stage2 -w test/*.py  # 使得test路径下的所有.py文件同时支持python2/3

更详细的使用教程参见:futurize(https://python-future.org)

当然这两种全自动的代码转换解决方案都有一定局限性,因为实际中代码情况千变万化,为了确保成功迁移,你应该每做一步转换就对代码的行为进行测试,测试中遇到的一些问题需要手动修改解决,当测试没问题后,再进行下一步转换。

场景三:写出python2/3兼容的package

这种场景在python2已然退休的今天应该已经成为伪需求了,但是为了让我们这里讨论的场景尽可能完备,还是把它加进来吧。在 python2和python3的区别这部分中,我们给出的python2和python3兼容的解决方案其实就是答案了。总体的思路如下:首先你需要了解两个版本中哪些地方有区别,然后利用future,six等兼容性解决package去统一这些区别,这样最后写出的package就是python2/3同时兼容的了。

04

结语

本篇文章主要介绍了python2和python3的兼容性问题(区别)、对应的解决方案以及py2/py3代码自动转换工具。这里面很多区别和场景都是根据我自己所遇到的情况总结的,所以肯定有不完善的地方,大家如果有好的建议和意见也欢迎下方留言评论。希望本文可以或多或少地帮助到你。最后很想分享一句老友送我的话与大家共勉:计算机的很多东西看似高深但是并没有那么难理解,因为这个领域的所有东西都是人来定义的,你很容易找到其中的逻辑来帮助你学习。

05

参考文献

[1] Six: Python 2 and 3 Compatibility Library

[2] Cheat Sheet: Writing Python 2-3 compatible code

[3] Automatic conversion to Py2/3

[4] Unicode HOWTO