Python2 和 Python3 的區別及相容技巧

pythontab發表於2018-10-26

前言

最近 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')


相關文章