前言
最近 Python 之父 Guido van Rossum(龜爺)終於在 Python 官方郵件組落實了 Python 2.7 的終焉之日(EOL)。
說的是 Python 2.7 的 EOL 日期最終確定為 2020 年 1 月 1 日,之後不會有任何更新,包括原始碼的安全補丁。
所以相容Python3已經可以說非常必要了,但有些常用的庫還沒有升級到Python3,所以我們看下如何寫出相容2和3的程式碼。
Python 2 or 3 ?
Python 3 被欽定為 Python 的未來,於 2008 年末釋出,是目前正在開發的版本。旨在解決和修正 Python 2 遺留的設計缺陷、清理程式碼庫冗餘、追求有且僅有一種最佳實踐方式來執行任務等問題。
起初,由於 Python 3 不能向後相容的事實,導致了使用者採用緩慢,對初學者不友好等問題。但在 Python 社群的努力和決絕態度下,截至龜爺發出郵件之前,已經有了 21903 個 Packages 可以支援 Python 3.5,其中包括了絕大多數最受歡迎的封裝庫,與此同時也有越來越多的封裝庫(e.g. Django、Numpy)表示其新版本將不再支援 Python 2。
Python 2.7 於 3.0 之後的 2010 年 7 月 3 日釋出,計劃作為 2.x 的最後一個版本。Python 2.7 的歷史任務在於透過提供 2 和 3 之間的相容性措施,使 Python 2.x 的使用者更容易將程式碼移植到 Python 3.x 上。那麼如果你希望自己的程式碼能夠相容兩個不同的版本,首先你起碼要讓程式碼能夠正常的執行在 Python 2.7 上。
注:下文使用 P2 表示 Python 2.7;使用 P3 表示 Python 3.x。
不同與相容
__future__ 模組是我們首先需要了解的,該模組最主要的作用是支援在 P2 中匯入那些在 P3 才生效的模組和函式。是一個非常優秀的相容性工具庫,在下文中給出的許多 相容技巧 例項都依賴於它。
特性 在此版本可選 在此版本內建 效果
nested_scopes 2.1.0b1 2.2 PEP 227:靜態巢狀作用域
generators 2.2.0a1 2.3 PEP 255:簡單生成器
division 2.2.0a2 3.0 PEP 238:除法運算子改動
absolute_import 2.5.0a1 3.0 PEP 328:Imports 多行匯入與絕對相對路徑
with_statement 2.5.0a1 2.6 PEP 343:with 語句
print_function 2.6.0a2 3.0 PEP 3105:print 語句升級為函式
unicode_literals 2.6.0a2 3.0 PEP 3112:Bytes 型別
(__future__ 功能列表)
統一不等於語法
P2 支援使用 <> 和 != 表示不等於。
P3 僅支援使用 != 表示不等於。
相容技巧:
統一使用 != 語法
統一整數型別
P2 中整數型別可以細分為短整型 int 和長整型 long。
P3 廢除了短整型,並統一使用 int 表示長整型(不再有 L 跟在 repr 後面)。
相容技巧:
# Python 2 only k = 9223372036854775808L # Python 2 and 3: k = 9223372036854775808
# Python 2 only bigint = 1L # Python 2 and 3 from future.builtins import int bigint = int(1)
統一整數除法
P2 的除法 / 符號實際上具有兩個功能:
當兩個運算元均為整型物件時,進行的是地板除(截除小數部分),返回整型物件;
當兩個運算元存在至少一個浮點型物件時,進行的是真除(保留小數部分),返回浮點型物件。
P3 的除法 / 符號僅僅具有真除的功能,而地板除的功能則交由 // 來完成。
相容技巧:
# Python 2 only: assert 2 / 3 == 0 # Python 2 and 3: assert 2 // 3 == 0 “True division” (float division):
# Python 3 only: assert 3 / 2 == 1.5 # Python 2 and 3: from __future__ import division # (at top of module)
統一縮排語法
P2 可以混合使用 tab 和 space 兩種方式來進行縮排(1 個 tab == 8 個 space),但實際上這一特性並非所有 IDE 都能夠支援,會因此出現同樣的程式碼無法跨 IDE 執行的情況。
P3 統一使用 tab 作為縮排,如果 tab 和 space 同時存在,就會觸發異常:
TabError: inconsistent use of tabs and spaces in indentation.
相容技巧:
統一使用 tab 作為縮排。
統一類定義
P2 同時支援新式類(object)和老式類。
P3 則統一使用新式類,並且只有使用新式類才能應用多重繼承。
相容技巧:
統一使用新式類。
統一字元編碼型別
P2 預設使用 ASCII 字元編碼,但因為 ASCII 只支援數百個字元,並不能靈活的滿足非英文字元,所以 P2 同時也支援 Unicode 這種更強大的字元編碼。不過,由於 P2 同時支援兩套字元編碼,就難免多出了一些標識和轉換的麻煩。
而 P3 統一使用 Unicode 字元編碼,這節省了開發者的時間,同時也可以輕鬆地在程式中輸入和顯示更多種類的字元。
相容技巧:
在所有的字串賦值中均使用字首 u,或引入 unicode_literals 字元模組。
# Python 2 only s1 = 'PythonTab' s2 = u'PythonTab中文網' # Python 2 and 3 s1 = u'PythonTab' s2 = u'PythonTab中文網' # Python 2 and 3 from __future__ import unicode_literals # at top of module s1 = 'PythonTab' s2 = 'PythonTab中文網'
統一匯入模組的路徑搜尋方式
P2 匯入一個模組時首先會搜尋當前目錄(cwd),若非,則搜尋環境變數路徑(sys.path)。這一特性時常給開發者帶來困擾,相信大家都曾經碰到過,尤其當自定義模組與系統模組重名的時候;
為了解決這個問題,預設的 P3 僅會搜尋環境變數路徑,當你需要搜尋自定義模組時,你可以在包管理模式下將專案路徑加入到環境變數中,然後再使用絕對路徑和相對路徑(以 . 開頭)的方式來匯入。
相容技巧:
統一使用絕對路徑進行自定義模組匯入。
修正列表推導式的變數作用域洩露
P2 的列表推倒式中的變數會洩露到全域性作用域,例如:
import platform print('Python', platform.python_version()) i = 1 print('before: I = %s' % i) print('comprehension: %s' % [i for i in range(5)]) print('after: I = %s' % i) # OUT Python 2.7.6 before: i = 1 comprehension: [0, 1, 2, 3, 4] after: i = 4
P3 則解決了這個問題,列表推倒式中的變數不再洩露到全域性作用域。
import platform print('Python', platform.python_version()) i = 1 print('before: i =', i) print('comprehension:', [i for i in range(5)]) print('after: i =', i) # OUT Python 3.4.1 before: i = 1 comprehension: [0, 1, 2, 3, 4] after: i = 1
修正非法比較操作異常
P2 能夠對兩個資料型別並不相同的物件進行比較。
import platform print('Python', platform.python_version()) print("[1, 2] > 'foo' = ", [1, 2] > 'foo') print("(1, 2) > 'foo' = ", (1, 2) > 'foo') print("[1, 2] > (1, 2) = ", [1, 2] > (1, 2))
# OUT
Python 2.7.6 [1, 2] > 'foo' = False (1, 2) > 'foo' = True [1, 2] > (1, 2) = False
不過,這種看似方便的特性,實際上卻是一個定時炸彈,因為你無法唯一的確定到底是什麼原因導致的返回值為 False(可能是資料比較、也可能是資料型別不一致)。
P3 則對其進行了修正,如果比較運算元型別不一致時,會觸發 TypeError 異常。
相容技巧:
永遠不要比較資料型別不一致的物件。
統一丟擲異常語法
P2 同時支援新舊兩種異常觸發語法:
raise IOError, "file error" # Old raise IOError("file error") # New
P3 則統一使用新異常觸發語法,否則會觸發 SyntaxError 異常:
raise IOError("file error")
相容技巧:
### 丟擲異常 # Python 2 only: raise ValueError, "dodgy value" # Python 2 and 3: raise ValueError("dodgy value") ### 使用 traceback 丟擲異常 # Python 2 only: traceback = sys.exc_info()[2] raise ValueError, "dodgy value", traceback # Python 3 only: raise ValueError("dodgy value").with_traceback() # Python 2 and 3: option 1 from six import reraise as raise_ # or # from future.utils import raise_ traceback = sys.exc_info()[2] raise_(ValueError, "dodgy value", traceback) # Python 2 and 3: option 2 from future.utils import raise_with_traceback raise_with_traceback(ValueError("dodgy value")) ### 異常鏈處理 # Setup: class DatabaseError(Exception): pass # Python 3 only class FileDatabase: def __init__(self, filename): try: self.file = open(filename) except IOError as exc: raise DatabaseError('failed to open') from exc # Python 2 and 3: from future.utils import raise_from class FileDatabase: def __init__(self, filename): try: self.file = open(filename) except IOError as exc: raise_from(DatabaseError('failed to open'), exc)
統一異常處理語法
P2 實現異常處理也能夠支援兩種語法。
try: let_us_cause_a_NameError except NameError, err: # except NameError as err: print err, '--> our error message'
P3 的異常處理則強制要求使用 as 關鍵字的方式。
try: let_us_cause_a_NameError except NameError as err: print(err, '--> our error message')
相容技巧:
統一使用 as 關鍵字的異常處理方式。
統一輸入函式
P2 支援 raw_input 和 input 兩個輸入函式,區別在於前者僅能返回 String 型別物件,後者則支援返回數字和字串兩種資料型別物件,並且當輸入為表示式時,會隱式呼叫 eval 函式返回其執行結果。顯然的,使用 input 是更加靈活的寫法。
所以 P3 統一的使用了 input 函式進行輸入處理。
相容技巧:
統一使用 input 內建函式。
# Python 2 only: input("Type something safe please: ") # Python 2 and 3 from future.builtins import input eval(input("Type something safe please: "))
統一輸出函式
P2 中的 print 即是關鍵字又是內建函式。print 'Hello world!' 為一條語句,print('Hello world!') 則為一次函式呼叫。
P3 統一使用 print 函式進行輸出操作,其原型如下,這一改變讓 P3 的輸出處理變得更加簡潔、強大而優雅,透過實參的傳遞就能替代 P2 中繁複的程式碼實現。
print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
相容技巧:
### 單行列印單個 String # Python 2 only: print 'Hello' # Python 2 only: print 'Hello' ### 單行列印多個 String # Python 2 only: print 'Hello', 'Guido' # Python 2 and 3: from __future__ import print_function # (at top of module) print('Hello', 'Guido') ### 輸出重定向 # Python 2 only: print >> sys.stderr, 'Hello' # Python 2 and 3: from __future__ import print_function print('Hello', file=sys.stderr) ### 換行列印 # Python 2 only: print 'Hello', # Python 2 and 3: from __future__ import print_function print('Hello', end='')
統一檔案操作函式
P2 支援使用 file 和 open 兩個函式來進行檔案操作。
P3 則統一使用 open 來進行檔案操作。
相容技巧:
統一使用 open 函式。
# Python 2 only: f = file(pathname) # Python 2 and 3: f = open(pathname)
統一列表迭代器生成函式
P2 支援使用 range 和 xrange 兩個函式來生成可迭代物件,區別在於前者返回的是一個列表型別物件,後者返回的是一個類似生成器(惰性求值)的迭代物件,支援無限迭代。所以當你需要生成一個很大的序列時,推薦使用 xrange,因為它不會一上來就索取序列所需的所有記憶體空間。如果只對序列進行讀操作的話,xrange 方法效率顯然會更高,但是如果要修改序列的元素,或者往序列增刪元素的話,那隻能透過 range 方法生成一個 list 物件了。
P3 則統一使用 range 函式來生成可迭代物件,但其實 P3 的 range 更像是 P2 的 xrange。所以在 P3 中如果你想得到一個可以被修改的列表物件,你需要這麼做:
list(range(1,10)) [1, 2, 3, 4, 5, 6, 7, 8, 9]
相容技巧:
統一使用 range 函式
# Python 2 only: for i in xrange(10**8): ... # Python 2 and 3: forward-compatible from future.builtins import range for i in range(10**8): ... # Python 2 and 3: backward-compatible from past.builtins import xrange for i in xrange(10**8): ...
統一迭代器迭代函式
P2 中支援使用內建函式 next 和迭代器物件的 .next() 例項方法這兩種方式來獲取迭代器物件的下一個元素。所以,在實現自定義迭代器物件類時,必須實現 .next() 例項方法:
# Python 2 only class Upper(object): def __init__(self, iterable): self._iter = iter(iterable) def next(self): # Py2-styface iterator interface return self._iter.next().upper() def __iter__(self): return self itr = Upper('hello') assert itr.next() == 'H' # Py2-style assert list(itr) == list('ELLO')
但在 P3 中統一了使用 next 內建函式來獲取下一個元素,如果試圖呼叫 .next() 方法則會觸發 AttributeError 異常。所以,在 P3 中實現自定義迭代器所要實現的是 __next__ 特殊方法。
相容技巧:
# Python 2 and 3: option 1 from future.builtins import object 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') # Python 2 and 3: option 2 from future.utils import implements_iterator @implements_iterator 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' assert list(itr) == list('ELLO')