目錄

前言

最近 Python 之父 Guido van Rossum(龜爺)終於在 Python 官方郵件組落實了 Python 2.7 的終焉之日(EOL)。

Let’s not play games with semantics. The way I see the situation for 2.7 is that EOL is January 1st, 2020, and there will be no updates, not even source-only security patches, after that date. Support (from the core devs, the PSF, and python.org) stops completely on that date. If you want support for 2.7 beyond that day you will have to pay a commercial vendor. Of course it’s open source so people are also welcome to fork it. But the core devs have toiled long enough, and the 2020 EOL date (an extension from the originally annouced 2015 EOL!) was announced with sufficient lead time and fanfare that I don’t feel bad about stopping to support it at all.

一言以蔽之,說的是 Python 2.7 的 EOL 日期最終確定為 2020 年 1 月 1 日,之後不會有任何更新,包括原始碼的安全補丁。

So,還沒有相容或移植到 Python 3.x 的程式,或許已經到了該提上日程的時候了。在下文中,我們就「選擇 Python 2 還是 3 ?兩者的主要區別是什麼?如何編寫相容兩者的程式碼?」這 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 3 已成氣候,正如龜爺在郵件中所說:「停止支援 Python 2,我完全沒覺得有啥糟糕的」。所以筆者認為,如果你喜歡 Python 並希望能夠掌握它,那麼你完全沒有必然去糾結於學習 2 還是 3 的問題。而且當我們在討論 Python 2.x 的問題時,實際上更多的是在討論 Python 2.7。

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 == 0True 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 = 'The Zen of Python'
s2 = u'きたないのよりきれいな方がいい\n'

# Python 2 and 3
s1 = u'The Zen of Python'
s2 = u'きたないのよりきれいな方がいい\n'

# Python 2 and 3
from __future__ import unicode_literals    # at top of module

s1 = 'The Zen of Python'
s2 = 'きたないのよりきれいな方がいい\n'

統一匯入模組的路徑搜尋方式

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