【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