作者| Alex Rogozhnikov
機(jī)器之心翻譯出品(http://t.cn/R8yJqkB)
對(duì)于數(shù)據(jù)科學(xué)開發(fā)者而言,如何將已有項(xiàng)目從 Python 2 轉(zhuǎn)向 Python 3 成為了正在面臨的重大問題。來(lái)自莫斯科大學(xué)的 Alex Rogozhnikov 博士為我們整理了一份代碼遷移指南。
目前,Python 科學(xué)棧中的所有主要項(xiàng)目都同時(shí)支持 Python 3.x 和 Python 2.7,不過,這種情況很快即將結(jié)束。去年 11 月,Numpy 團(tuán)隊(duì)的一份聲明引發(fā)了數(shù)據(jù)科學(xué)社區(qū)的關(guān)注:這一科學(xué)計(jì)算庫(kù)即將放棄對(duì)于 Python 2.7 的支持,全面轉(zhuǎn)向 Python 3。Numpy 并不是唯一宣稱即將放棄 Python 舊版本支持的工具,pandas 與 Jupyter notebook 等很多產(chǎn)品也在即將放棄支持的名單之中。對(duì)于數(shù)據(jù)科學(xué)開發(fā)者而言,如何將已有項(xiàng)目從 Python 2 轉(zhuǎn)向 Python 3 成為了正在面臨的重大問題。來(lái)自莫斯科大學(xué)的 Alex Rogozhnikov 博士為我們整理了一份代碼遷移指南。
Python 3 功能簡(jiǎn)介
Python 是機(jī)器學(xué)習(xí)和其他科學(xué)領(lǐng)域中的主流語(yǔ)言,我們通常需要使用它處理大量的數(shù)據(jù)。Python 兼容多種深度學(xué)習(xí)框架,且具備很多優(yōu)秀的工具來(lái)執(zhí)行數(shù)據(jù)預(yù)處理和可視化。
但是,Python 2 和 Python 3 長(zhǎng)期共存于 Python 生態(tài)系統(tǒng)中,很多數(shù)據(jù)科學(xué)家仍然使用 Python 2。2019 年底,Numpy 等很多科學(xué)計(jì)算工具都將停止支持 Python 2,而 2018 年后 Numpy 的所有新功能版本將只支持 Python 3。
為了使 Python 2 向 Python 3 的轉(zhuǎn)換更加輕松,我收集了一些 Python 3 的功能,希望對(duì)大家有用。
使用 pathlib 更好地處理路徑
pathlib 是 Python 3 的默認(rèn)模塊,幫助避免使用大量的 os.path.joins:

Python 2 總是試圖使用字符串級(jí)聯(lián)(準(zhǔn)確,但不好),現(xiàn)在有了 pathlib,代碼安全、準(zhǔn)確、可讀性強(qiáng)。
此外,pathlib.Path 具備大量方法,這樣 Python 新用戶就不用每個(gè)方法都去搜索了:

pathlib 會(huì)節(jié)約大量時(shí)間,詳見:
類型提示(Type hinting)成為語(yǔ)言的一部分
PyCharm 中的類型提示示例:
Python 不只是適合腳本的語(yǔ)言,現(xiàn)在的數(shù)據(jù)流程還包括大量步驟,每一步都包括不同的框架(有時(shí)也包括不同的邏輯)。
類型提示被引入 Python,以幫助處理越來(lái)越復(fù)雜的項(xiàng)目,使機(jī)器可以更好地進(jìn)行代碼驗(yàn)證。而之前需要不同的模塊使用自定義方式在文檔字符串中指定類型(注意:PyCharm 可以將舊的文檔字符串轉(zhuǎn)換成新的類型提示)。
下列代碼是一個(gè)簡(jiǎn)單示例,可以處理不同類型的數(shù)據(jù)(這就是我們喜歡 Python 數(shù)據(jù)棧之處)。

上述代碼適用于 numpy.array(包括多維)、astropy.Table 和 astropy.Column、bcolz、cupy、mxnet.ndarray 等。
該代碼同樣可用于 pandas.Series,但是方式是錯(cuò)誤的:

這是一個(gè)兩行代碼。想象一下復(fù)雜系統(tǒng)的行為多么難預(yù)測(cè),有時(shí)一個(gè)函數(shù)就可能導(dǎo)致錯(cuò)誤的行為。明確了解哪些類型方法適合大型系統(tǒng)很有幫助,它會(huì)在函數(shù)未得到此類參數(shù)時(shí)給出提醒。

如果你有一個(gè)很棒的代碼庫(kù),類型提示工具如 MyPy 可能成為集成流程中的一部分。不幸的是,提示沒有強(qiáng)大到足以為 ndarrays/tensors 提供細(xì)粒度類型,但是或許我們很快就可以擁有這樣的提示工具了,這將是 DS 的偉大功能。
類型提示 → 運(yùn)行時(shí)的類型檢查
默認(rèn)情況下,函數(shù)注釋不會(huì)影響代碼的運(yùn)行,不過它也只能幫你指出代碼的意圖。
但是,你可以在運(yùn)行時(shí)中使用 enforce 等工具強(qiáng)制進(jìn)行類型檢查,這可以幫助你調(diào)試代碼(很多情況下類型提示不起作用)。

函數(shù)注釋的其他用處
如前所述,注釋不會(huì)影響代碼執(zhí)行,而且會(huì)提供一些元信息,你可以隨意使用。
例如,計(jì)量單位是科學(xué)界的一個(gè)普遍難題,astropy 包提供一個(gè)簡(jiǎn)單的裝飾器(Decorator)來(lái)控制輸入量的計(jì)量單位,并將輸出轉(zhuǎn)換成所需單位。

如果你擁有 Python 表格式科學(xué)數(shù)據(jù)(不必要太多),你應(yīng)該嘗試一下 astropy。你還可以定義針對(duì)某個(gè)應(yīng)用的裝飾器,用同樣的方式來(lái)控制/轉(zhuǎn)換輸入和輸出。
通過 @ 實(shí)現(xiàn)矩陣乘法
下面,我們實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的機(jī)器學(xué)習(xí)模型,即帶 L2 正則化的線性回歸:

下面 Python 3 帶有 @ 作為矩陣乘法的符號(hào)更具有可讀性,且更容易在深度學(xué)習(xí)框架中轉(zhuǎn)譯:因?yàn)橐恍┤?X @ W + b[None, :] 的代碼在 numpy、cupy、pytorch 和 tensorflow 等不同庫(kù)下都表示單層感知機(jī)。
使用 ** 作為通配符
遞歸文件夾的通配符在 Python2 中并不是很方便,因此才存在定制的 glob2 模塊來(lái)克服這個(gè)問題。遞歸 flag 在 Python 3.6 中得到了支持。

python3 中更好的選擇是使用 pathlib:

Print 在 Python3 中是函數(shù)
Python 3 中使用 Print 需要加上麻煩的圓括弧,但它還是有一些優(yōu)點(diǎn)。
使用文件描述符的簡(jiǎn)單句法:

在不使用 str.join 下輸出 tab-aligned 表格:

修改與重新定義 print 函數(shù)的輸出:

在 Jupyter 中,非常好的一點(diǎn)是記錄每一個(gè)輸出到獨(dú)立的文檔,并在出現(xiàn)錯(cuò)誤的時(shí)候追蹤出現(xiàn)問題的文檔,所以我們現(xiàn)在可以重寫 print 函數(shù)了。
在下面的代碼中,我們可以使用上下文管理器暫時(shí)重寫 print 函數(shù)的行為:

上面并不是一個(gè)推薦的方法,因?yàn)樗鼤?huì)引起系統(tǒng)的不穩(wěn)定。
print 函數(shù)可以加入列表解析和其它語(yǔ)言構(gòu)建結(jié)構(gòu)。

f-strings 可作為簡(jiǎn)單和可靠的格式化
默認(rèn)的格式化系統(tǒng)提供了一些靈活性,且在數(shù)據(jù)實(shí)驗(yàn)中不是必須的。但這樣的代碼對(duì)于任何修改要么太冗長(zhǎng),要么就會(huì)變得很零碎。而代表性的數(shù)據(jù)科學(xué)需要以固定的格式迭代地輸出一些日志信息,通常需要使用的代碼如下:

樣本輸出:

f-strings 即格式化字符串在 Python 3.6 中被引入:

另外,寫查詢語(yǔ)句時(shí)非常方便:

「true division」和「integer division」之間的明顯區(qū)別
對(duì)于數(shù)據(jù)科學(xué)來(lái)說這種改變帶來(lái)了便利(但我相信對(duì)于系統(tǒng)編程來(lái)說不是)。

Python 2 中的結(jié)果依賴于『時(shí)間』和『距離』(例如,以米和秒為單位)是否被保存為整數(shù)。
在 Python 3 中,結(jié)果的表示都是精確的,因?yàn)槌ǖ慕Y(jié)果是浮點(diǎn)數(shù)。
另一個(gè)案例是整數(shù)除法,現(xiàn)在已經(jīng)作為明確的運(yùn)算:

注意:該運(yùn)算可以應(yīng)用到內(nèi)建類型和由數(shù)據(jù)包(例如,numpy 或 pandas)提供的自定義類型。
嚴(yán)格排序

防止不同類型實(shí)例的偶然性的排序。

在處理原始數(shù)據(jù)時(shí)幫助發(fā)現(xiàn)存在的問題。
旁注:對(duì) None 的合適檢查是(兩個(gè)版本的 Python 都適用):

自然語(yǔ)言處理的 Unicode

輸出:
Python 2: 6\n??
Python 3: 2\n 您好.

Python 2 在此失敗了,而 Python 3 可以如期工作(因?yàn)槲以谧址惺褂昧硕砦淖帜福?/p>
在 Python 3 中 strs 是 Unicode 字符串,對(duì)非英語(yǔ)文本的 NLP 處理更加方便。
還有其它有趣的方面,例如:


Python 2:
Counter({'\xc3': 2, 'b': 1, 'e': 1, 'c': 1, 'k': 1, 'M': 1, 'l': 1, 's': 1, 't': 1, '\xb6': 1, '\xbc': 1})Python 3:
Counter({'M': 1, '?': 1, 'b': 1, 'e': 1, 'l': 1, 's': 1, 't': 1, 'ü': 1, 'c': 1, 'k': 1})
這些在 Python 2 里也能正確地工作,但 Python 3 更為友好。
保留詞典和**kwargs 的順序
在 CPython 3.6+ 版本中,字典的默認(rèn)行為類似于 OrderedDict(在 3.7+版本中已得到保證)。這在字典理解(和其他操作如 json 序列化/反序列化期間)保持順序。

它同樣適用于**kwargs(在 Python 3.6+版本中):它們的順序就像參數(shù)中顯示的那樣。當(dāng)設(shè)計(jì)數(shù)據(jù)流程時(shí),順序至關(guān)重要,以前,我們必須以這樣繁瑣的方式來(lái)編寫:

注意到了嗎?名稱的唯一性也會(huì)被自動(dòng)檢查。
迭代地拆封

默認(rèn)的 pickle 引擎為數(shù)組提供更好的壓縮

節(jié)省 3 倍空間,而且速度更快。實(shí)際上,類似的壓縮(不過與速度無(wú)關(guān))可以通過 protocol=2 參數(shù)來(lái)實(shí)現(xiàn),但是用戶通常會(huì)忽略這個(gè)選項(xiàng)(或者根本不知道)。
更安全的解析

關(guān)于 super()
Python 2 的 super(...)是代碼錯(cuò)誤中的常見原因。

關(guān)于 super 和方法解析順序的更多內(nèi)容,參見 stackoverflow:https://stackoverflow.com/questions/576169/understanding-python-super-with-init-methods
更好的 IDE 會(huì)給出變量注釋
在使用 Java、C# 等語(yǔ)言編程的過程中最令人享受的事情是 IDE 可以提供非常好的建議,因?yàn)樵趫?zhí)行代碼之前,所有標(biāo)識(shí)符的類型都是已知的。
而在 Python 中這很難實(shí)現(xiàn),但是注釋可以幫助你:
以清晰的形式寫下你的期望
從 IDE 獲取良好的建議
這是一個(gè)帶變量注釋的 PyCharm 示例。即使你使用的函數(shù)不帶注釋(例如,由于向后兼容性),它也能工作。
多種拆封(unpacking)
在 Python3 中融合兩個(gè)字典的代碼示例:

可以在這個(gè)鏈接中查看 Python2 中的代碼對(duì)比:https://stackoverflow.com/questions/38987/how-to-merge-two-dictionaries-in-a-single-expression
aame 方法對(duì)于列表(list)、元組(tuple)和集合(set)都是有效的(a、b、c 是任意的可迭代對(duì)象):

對(duì)于*args 和 **kwargs,函數(shù)也支持額外的 unpacking:

只帶關(guān)鍵字參數(shù)的 API
我們考慮這個(gè)代碼片段:

很明顯,代碼的作者還沒熟悉 Python 的代碼風(fēng)格(很可能剛從 cpp 和 rust 跳到 Python)。不幸的是,這不僅僅是個(gè)人偏好的問題,因?yàn)樵?SVC 中改變參數(shù)的順序(adding/deleting)會(huì)使得代碼無(wú)效。特別是,sklearn 經(jīng)常會(huì)重排序或重命名大量的算法參數(shù)以提供一致的 API。每次重構(gòu)都可能使代碼失效。
在 Python3,庫(kù)的編寫者可能需要使用*以明確地命名參數(shù):

現(xiàn)在,用戶需要明確規(guī)定參數(shù) sklearn.svm.SVC(C=2, kernel='poly', degree=2, gamma=4, coef0=0.5) 的命名。
這種機(jī)制使得 API 同時(shí)具備了可靠性和靈活性。
小調(diào):math 模塊中的常量

小調(diào):?jiǎn)尉日麛?shù)類型
Python 2 提供了兩個(gè)基本的整數(shù)類型,即 int(64 位符號(hào)整數(shù))和用于長(zhǎng)時(shí)間計(jì)算的 long(在 C++變的相當(dāng)莫名其妙)。
Python 3 有一個(gè)單精度類型的 int,它包含了長(zhǎng)時(shí)間的運(yùn)算。
下面是查看值是否是整數(shù)的方法:

其他
Enums 有理論價(jià)值,但是字符串輸入已廣泛應(yīng)用在 python 數(shù)據(jù)棧中。Enums 似乎不與 numpy 交互,并且不一定來(lái)自 pandas。
協(xié)同程序也非常有希望用于數(shù)據(jù)流程,但還沒有出現(xiàn)大規(guī)模應(yīng)用。
Python 3 有穩(wěn)定的 ABI
Python 3 支持 unicode(因此ω = Δφ / Δt 也 okay),但你最好使用好的舊的 ASCII 名稱
一些庫(kù)比如 jupyterhub(jupyter in cloud)、django 和新版 ipython 只支持 Python 3,因此對(duì)你來(lái)講沒用的功能對(duì)于你可能只想使用一次的庫(kù)很有用。
數(shù)據(jù)科學(xué)特有的代碼遷移問題(以及如何解決它們)
停止對(duì)嵌套參數(shù)的支持:

然而,它依然完美適用于不同的理解:

通常,理解在 Python 2 和 3 之間可以更好地「翻譯」。
map(), .keys(), .values(), .items(), 等等返回迭代器,而不是列表。迭代器的主要問題有:沒有瑣碎的分割和無(wú)法迭代兩次。將結(jié)果轉(zhuǎn)化為列表幾乎可以解決所有問題。
遇到問題請(qǐng)參見 Python 問答:我如何移植到 Python 3?(https://eev.ee/blog/2016/07/31/python-faq-how-do-i-port-to-python-3/)
用 python 教機(jī)器學(xué)習(xí)和數(shù)據(jù)科學(xué)的主要問題
課程作者應(yīng)該首先花時(shí)間解釋什么是迭代器,為什么它不能像字符串那樣被分片/級(jí)聯(lián)/相乘/迭代兩次(以及如何處理它)。
我相信大多數(shù)課程作者很高興避開這些細(xì)節(jié),但是現(xiàn)在幾乎不可能。
結(jié)論
Python 2 與 Python 3 共存了近 10 年,時(shí)至今日,我們必須要說:是時(shí)候轉(zhuǎn)向 Python 3 了。
研究和生產(chǎn)代碼應(yīng)該更短,更易讀取,并且在遷移到 Python 3 代碼庫(kù)之后明顯更加的安全。
現(xiàn)在大多數(shù)庫(kù)同時(shí)支持 2.x 和 3.x 兩個(gè)版本。但我們不應(yīng)等到流行工具包開始停止支持 Python 2 才開始行動(dòng),提前享受新語(yǔ)言的功能吧。
遷移過后,我敢保證程序會(huì)更加順暢:「我們不會(huì)再做向后不兼容的事情了(https://snarky.ca/why-python-3-exists/)」。