最好的Python入門進階教程,1個月輕鬆掌握Python 六大核心知識點

隨着機器學習的興起,Python 逐步成爲了「最受歡迎」的語言。它簡單易用、邏輯明確並擁有海量的擴展包,因此其不僅成爲機器學習與數據科學的首選語言,同時在網頁、數據爬取可科學研究等方面成爲不二選擇。此外,很多入門級的機器學習開發者都是跟隨大流選擇 Python,但到底爲什麼要選擇 Python 就是本文的核心內容。

PS:文章有點長,建議收藏後在觀看!感謝大家觀看!

本教程的目的是讓你相信兩件事:首先,Python 是一種非常棒的編程語言;其次,如果你是一名科學家,Python 很可能值得你去學習。本教程並非想要說明 Python 是一種萬能的語言;相反,作者明確討論了在幾種情況下,Python 並不是一種明智的選擇。本教程的目的只是提供對 Python 一些核心特徵的評論,並闡述作爲一種通用的科學計算語言,它比其他常用的替代方案(最著名的是 R 和 Matlab)更有優勢。

本教程的其餘部分假定你已經有了一些編程經驗,如果你非常精通其他以數據爲中心的語言(如 R 或 Matlab),理解本教程就會非常容易。本教程不能算作一份關於 Python 的介紹,且文章重點在於爲什麼應該學習 Python 而不是怎樣寫 Python 代碼(儘管其他地方有大量的優秀教程)。

概述

Python 是一種廣泛使用、易於學習、高級、通用的動態編程語言。這很令人滿意,所以接下來分開討論一些特徵。

Python(相對來說)易於學習

編程很難,因此從絕對意義上來說,除非你已經擁有編程經驗,否則編程語言難以學習。但是,相對而言,Python 的高級屬性(見下一節)、語法可讀性和語義直白性使得它比其他語言更容易學習。例如,這是一個簡單 Python 函數的定義(故意未註釋),它將一串英語單詞轉換爲(crummy)Pig Latin:

以上函數事實上無法生成完全有效的 Pig Latin(假設存在「有效 Pig Latin」),但這沒有關係。有些情況下它是可行的:

拋開 Pig Latin 不說,這裏的重點只是,出於幾個原因,代碼是很容易閱讀的。首先,代碼是在高級抽象中編寫的(下面將詳細介紹),因此每行代碼都會映射到一個相當直觀的操作。這些操作可以是「取這個單詞的第一個字符」,而不是映射到一個沒那麼直觀的低級操作,例如「爲一個字符預留一個字節的內存,稍後我會傳入一個字符」。其次,控制結構(如,for—loops,if—then 條件等)使用諸如「in」,「and」和「not」的簡單單詞,其語義相對接近其自然英語含義。第三,Python 對縮進的嚴格控制強加了一種使代碼可讀的規範,同時防止了某些常見的錯誤。第四,Python 社區非常強調遵循樣式規定和編寫「Python 式的」代碼,這意味着相比使用其他語言的程序員而言,Python 程序員更傾向於使用一致的命名規定、行的長度、編程習慣和其他許多類似特徵,它們共同使別人的代碼更易閱讀(儘管這可以說是社區的一個特徵而不是語言本身)。在這裏相信有許多想要學習Python的同學,大家可以+下Python學習分享裙:五二八 三九七 六一七,即可免費領取一整套系統的 Python學習教程!

Python 是一種高級語言

與其他許多語言相比,Python 是一種相對「高級」的語言:它不需要(並且在許多情況下,不允許)用戶擔心太多底層細節,而這是其他許多語言需要去處理的。例如,假設我們想創建一個名爲「my_box_of_things」的變量當作我們所用東西的容器。我們事先不知道我們想在盒子中保留多少對象,同時我們希望在添加或刪除對象時,對象數量可以自動增減。所以這個盒子需要佔據一個可變的空間:在某個時間點,它可能包含 8 個對象(或「元素」),而在另一個時間點,它可能包含 257 個對象。在像 C 這樣的底層語言中,這個簡單的要求就已經給我們的程序帶來了一些複雜性,因爲我們需要提前聲明盒子需要佔據多少空間,然後每次我們想要增加盒子需要的空間時,我麼需要明確創建一個佔據更多空間的全新的盒子,然後將所有東西拷貝到其中。

相比之下,在 Python 中,儘管在底層這些過程或多或少會發生(效率較低),但我們在使用高級語言編寫時並不需要擔心這一部分。從我們的角度來看,我們可以創建自己的盒子並根據喜好添加或刪除對象:

 

更一般來說,Python(以及根據定義的其他所有高級語言)傾向於隱藏需要在底層語言中明確表達的各種死記硬背的聲明。這使得我們可以編寫非常緊湊、清晰的代碼(儘管它通常以降低性能爲代價,因爲內部不再可訪問,因此優化變得更加困難)。

例如,考慮從文件中讀取純文本這樣看似簡單的行爲。對於與文件系統直接接觸而傷痕累累的開發者來說,從概念上看似乎只需要兩個簡單的操作就可以完成:首先打開一個文件,然後從其中讀取。實際過程遠不止這些,並且比 Python 更底層的語言通常強制(或至少是鼓勵)我們去承認這一點。例如,這是在 Java 中從文件中讀取內容的規範(儘管肯定不是最簡潔的)方法:

你可以看到我們不得不做一些令人苦惱的事,例如導入文件讀取器、爲文件中的內容創建一個緩存,以塊的形式讀取文件塊並將它們分配到緩存中等等。相比之下,在 Python 中,讀取文件中的全部內容只需要如下代碼:

當然,這種簡潔性並不是 Python 獨有的;還有其他許多高級語言同樣隱藏了簡單請求所暗含的大部分令人討厭的內部過程(如,Ruby,R,Haskell 等)。但是,相對來說比較少有其他語言能與接下來探討的 Python 特徵相媲美。

Python 是一種通用語言

根據設計,Python 是一種通用的語言。也就是說,它旨在允許程序員在任何領域編寫幾乎所有類型的應用,而不是專注於一類特定的問題。在這方面,Python 可以與(相對)特定領域的語言進行對比,如 R 或 PHP。這些語言原則上可用於很多情形,但仍針對特定用例進行了明確優化(在這兩個示例中,分別用於統計和網絡後端開發)。

Python 通常被親切地成爲「所有事物的第二個最好的語言」,它很好地捕捉到了這樣的情緒,儘管在很多情況下 Python 並不是用於特定問題的最佳語言,但它通常具有足夠的靈活性和良好的支持性,使得人們仍然可以相對有效地解決問題。事實上,Python 可以有效地應用於許多不同的應用中,這使得學習 Python 成爲一件相當有價值的事。因爲作爲一個軟件開發人員,能夠使用單一語言實現所有事情,而不是必須根據所執行的項目在不同語言和環境間進行切換,是一件非常棒的事。

標準庫

通過瀏覽標準庫中可用的衆多模塊列表,即 Python 解釋器自帶的工具集(沒有安裝第三方軟件包),這可能是最容易理解 Python 通用性的方式。若考慮以下幾個示例:

os: 系統操作工具

re:正則表達

collections:有用的數據結構

multiprocessing:簡單的並行化工具

pickle:簡單的序列化

json:讀和寫 JSON

argparse:命令行參數解析

functools:函數化編程工具

datetime:日期和時間函數

cProfile:分析代碼的基本工具

這張列表乍一看並不令人印象深刻,但對於 Python 開發者來說,使用它們是一個相對常見的經歷。很多時候用谷歌搜索一個看似重要甚至有點深奧的問題,我們很可能找到隱藏在標準庫模塊內的內置解決方案。

JSON,簡單的方法

例如,假設你想從 web.JSON 中讀取一些 JSON 數據,如下所示:

我們可以花一些時間自己編寫 json 解析器,或試着去找一個有效讀取 json 的第三方包。但我們很可能是在浪費時間,因爲 Python 內置的 json 模塊已經能完全滿足我們的需要:

 

請注意,在我們能於 json 模塊內使用 loads 函數前,我們必須導入 json 模塊。這種必須將幾乎所有功能模塊明確地導入命名空間的模式在 Python 中相當重要,且基本命名空間中可用的內置函數列表非常有限。許多用過 R 或 Matlab 的開發者會在剛接觸時感到惱火,因爲這兩個包的全局命名空間包含數百甚至上千的內置函數。但是,一旦你習慣於輸入一些額外字符,它就會使代碼更易於讀取和管理,同時命名衝突的風險(R 語言中經常出現)被大大降低。

優異的外部支持

當然,Python 提供大量內置工具來執行大量操作並不意味着總需要去使用這些工具。可以說比 Python 豐富的標準庫更大的賣點是龐大的 Python 開發者社區。多年來,Python 一直是世界上最流行的動態編程語言,開發者社區也貢獻了衆多高質量的安裝包。

如下 Python 軟件包在不同領域內提供了被廣泛使用的解決方案(這個列表在你閱讀本文的時候可能已經過時了!):

Web 和 API 開發:flask,Django,Falcon,hug

爬取數據和解析文本/標記: requests,beautifulsoup,scrapy

自然語言處理(NLP):nltk,gensim,textblob

數值計算和數據分析:numpy,scipy,pandas,xarray

機器學習:scikit-learn,Theano,Tensorflow,keras

圖像處理:pillow,scikit-image,OpenCV

作圖:matplotlib,seaborn,ggplot,Bokeh

等等

Python 的一個優點是有出色的軟件包管理生態系統。雖然在 Python 中安裝包通常比在 R 或 Matlab 中更難,這主要是因爲 Python 包往往具有高度的模塊化和/或更多依賴於系統庫。但原則上至少大多數 Python 的包可以使用 pip 包管理器通過命令提示符安裝。更復雜的安裝程序和包管理器,如 Anaconda 也大大減少了配置新 Python 環境時產生的痛苦。

Python 是一種(相對)快速的語言

這可能令人有點驚訝:從表面上看,Python 是一種快速語言的說法看起來很愚蠢。因爲在標準測試時,和 C 或 Java 這樣的編譯語言相比,Python 通常會卡頓。毫無疑問,如果速度至關重要(例如,你正在編寫 3D 圖形引擎或運行大規模的流體動力學模擬實驗),Python 可能不會成爲你最優選擇的語言,甚至不會是第二好的語言。但在實際中,許多科學家工作流程中的限制因素不是運行時間而是開發時間。一個花費一個小時運行但只需要 5 分鐘編寫的腳本通常比一個花費 5 秒鐘運行但是需要一個禮拜編寫和調試的腳本更合意。此外,正如我們將在下面看到的,即使我們所用的代碼都用 Python 編寫,一些優化操作通常可以使其運行速度幾乎與基於 C 的解決方案一樣快。實際上,對大多數科學家家來說,基於 Python 的解決方案不夠快的情況並不是很多,而且隨着工具的改進,這種情況的數量正在急劇減少。

不要重複做功

軟件開發的一般原則是應該儘可能避免做重複工作。當然,有時候是沒法避免的,並且在很多情況下,爲問題編寫自己的解決方案或創建一個全新的工具是有意義的。但一般來說,你自己編寫的 Python 代碼越少,性能就越好。有以下幾個原因:

Python 是一種成熟的語言,所以許多現有的包有大量的用戶基礎並且經過大量優化。例如,對 Python 中大多數核心科學庫(numpy,scipy,pandas 等)來說都是如此。

大多數 Python 包實際上是用 C 語言編寫的,而不是用 Python 編寫的。對於大多數標準庫,當你調用一個 Python 函數時,實際上很大可能你是在運行具有 Python 接口的 C 代碼。這意味着無論你解決問題的算法有多精妙,如果你完全用 Python 編寫,而內置的解決方案是用 C 語言編寫的,那你的性能可能不如內置的方案。例如,以下是運行內置的 sum 函數(用 C 編寫):

 

 

從算法上來說,你沒有太多辦法來加速任意數值列表的加和計算。所以你可能會想這是什麼鬼,你也許可以用 Python 自己寫加和函數,也許這樣可以封裝內置 sum 函數的開銷,以防它進行任何內部驗證。嗯……並非如此。

 

至少在這個例子中,運行你自己簡單的代碼很可能不是一個好的解決方案。但這不意味着你必須使用內置 sum 函數作爲 Python 中的性能上限!由於 Python 沒有針對涉及大型輸入的數值運算進行優化,因此內置方法在加和大型列表時是表現次優。在這種情況下我們應該做的是提問:「是否有其他一些 Python 庫可用於對潛在的大型輸入進行數值分析?」正如你可能想的那樣,答案是肯定的:NumPy 包是 Python 的科學生態系統中的主要成分,Python 中的絕大多數科學計算包都以某種方式構建在 NumPy 上,它包含各種能幫助我們的計算函數。

在這種情況下,新的解決方案是非常簡單的:如果我們將純 Python 列表轉化爲 NumPy 數組,我們就可以立即調用 NumPy 的 sum 方法,我們可能期望它應該比核心的 Python 實現更快(技術上講,我們可以傳入一個 Python 列表到 numpy.sum 中,它會隱式地將其轉換爲數組,但如果我們打算複用該 NumPy 數組,最好明確地轉化它)。

因此簡單地切換到 NumPy 可加快一個數量級的列表加和速度,而不需要自己去實現任何東西。

需要更快的速度?

當然,有時候即使使用所有基於 C 的擴展包和高度優化的實現,你現有的 Python 代碼也無法快速削減時間。在這種情況下,你的下意識反應可能是放棄並轉化到一個「真正」的語言。並且通常,這是一種完全合理的本能。但是在你開始使用 C 或 Java 移植代碼前,你需要考慮一些不那麼費力的方法。

使用 Python 編寫 C 代碼

首先,你可以嘗試編寫 Cython 代碼。Cython 是 Python 的一個超集(superset),它允許你將(某些)C 代碼直接嵌入到 Python 代碼中。Cython 不以編譯的方式運行,相反你的 Python 文件(或其中特定的某部分)將在運行前被編譯爲 C 代碼。實際的結果是你可以繼續編寫看起來幾乎完全和 Python 一樣的代碼,但仍然可以從 C 代碼的合理引入中獲得性能提升。特別是簡單地提供 C 類型的聲明通常可以顯著提高性能。

以下是我們簡單加和代碼的 Cython 版本:

 

 

 

關於 Cython 版本有幾點需要注意一下。首先,在你第一次執行定義該方法的單元時,需要很少的(但值得注意的)時間來編譯。那是因爲,與純粹的 Python 不同,代碼在執行時不是逐行解譯的;相反,Cython 式的函數必須先編譯成 C 代碼才能調用。

其次,雖然 Cython 式的加和函數比我們上面寫的簡單的 Python 加和函數要快,但仍然比內置求和方法和 NumPy 實現慢得多。然而,這個結果更有力地說明了我們特定的實現過程和問題的本質,而不是 Cython 的一般好處;在許多情況下,一個有效的 Cython 實現可以輕易地將運行時間提升一到兩個數量級。

使用 NUMBA 進行清理

Cython 並不是提升 Python 內部性能的唯一方法。從開發的角度來看,另一種更簡單的方法是依賴於即時編譯,其中一段 Python 代碼在第一次調用時被編譯成優化的 C 代碼。近年來,在 Python 即時編譯器上取得了很大進展。也許最成熟的實現可以在 numba 包中找到,它提供了一個簡單的 jit 修飾器,可以輕易地結合其他任何方法。

我們之前的示例並沒有強調 JITs 可以產生多大的影響,所以我們轉向一個稍微複雜點的問題。這裏我們定義一個被稱爲 multiply_randomly 的新函數,它將一個一維浮點數數組作爲輸入,並將數組中的每個元素與其他任意一個隨機選擇的元素相乘。然後它返回所有隨機相乘的元素和。

讓我們從定義一個簡單的實現開始,我們甚至都不採用向量化來代替隨機相乘操作。相反,我們簡單地遍歷數組中的每個元素,從中隨機挑選一個其他元素,將兩個元素相乘並將結果分配給一個特定的索引。如果我們用基準問題測試這個函數,我們會發現它運行得相當慢。

在我們即時編譯之前,我們應該首先自問是否上述函數可以用更加符合 NumPy 形式的方法編寫。NumPy 針對基於數組的操作進行了優化,因此應該不惜一切代價地避免使用循環操作,因爲它們會非常慢。幸運的是,我們的代碼非常容易向量化(並且易於閱讀):

 

 

在作者的機器上,向量化版本的運行速度比循環版本的代碼快大約 100 倍。循環和數組操作之間的這種性能差異對於 NumPy 來說是非常典型的,因此我們要在算法上思考你所做的事的重要性。

假設我們不是花時間重構我們樸素的、緩慢的實現,而是簡單地在我們的函數上加一個修飾器去告訴 numba 庫我們要在第一次調用它時將函數編譯爲 C。字面上,下面的函數 multiply_randomly_naive_jit 與上面定義的函數 multiply_randomly_naive 之間的唯一區別是 @jit 修飾器。當然,4 個小字符是沒法造成那麼大的差異的。對吧?

令人驚訝的是,JIT 編譯版本的樸素函數事實上比向量化的版本跑得更快。

有趣的是,將 @jit 修飾器應用於函數的向量化版本(將其作爲聯繫留給讀者)並不能提供更多幫助。在 numba JIT 編譯器用於我們的代碼之後,Python 實現的兩個版本都以同樣的速度運行。因此,至少在這個例子中,即時編譯不僅可以毫不費力地爲我們提供類似 C 的速度,而且可以避免以 Python 式地去優化代碼。

這可能是一個相當有力的結論,因爲(a)現在 numba 的 JIT 編譯器只覆蓋了 NumPy 特徵的一部分,(b)不能保證編譯的代碼一定比解譯的代碼運行地更快(儘管這通常是一個有效的假設)。這個例子真正的目的是提醒你,在你宣稱它慢到無法去實現你想要做的事之前,其實你在 Python 中有許多可用的選擇。值得注意的是,如 C 集成和即時編譯,這些性能特徵都不是 Python 獨有的。Matlab 最近的版本自動使用即時編譯,同時 R 支持 JIT 編譯(通過外部庫)和 C ++ 集成(Rcpp)。

Python 是天生面向對象的

即使你正在做的只是編寫一些簡短的腳本去解析文本或挖掘一些數據,Python 的許多好處也很容易領會到。在你開始編寫相對大型的代碼片段前,Python 的最佳功能之一可能並不明顯:Python 具有設計非常優雅的基於對象的數據模型。事實上,如果你查看底層,你會發現 Python 中的一切都是對象。甚至函數也是對象。當你調用一個函數的時候,你事實上正在調用 Python 中每個對象都運行的 call 方法:

 

事實上,因爲 Python 中的一切都是對象,Python 中的所有內容遵循相同的核心邏輯,實現相同的基本 API,並以類似的方式進行擴展。對象模型也恰好非常靈活:可以很容易地定義新的對象去實現有意思的事,同時仍然表現得相對可預測。也許並不奇怪,Python 也是編寫特定領域語言(DSLs)的一個絕佳選擇,因爲它允許用戶在很大程度上重載和重新定義現有的功能。

魔術方法

Python 對象模型的核心部分是它使用「魔術」方法。這些在對象上實現的特殊方法可以更改 Python 對象的行爲——通常以重要的方式。魔術方法(Magic methods)通常以雙下劃線開始和結束,一般來說,除非你知道自己在做什麼,否則不要輕易篡改它們。但一旦你真的開始改了,你就可以做些相當了不起的事。

舉個簡單的例子,我們來定義一個新的 Brain 對象。首先,Barin 不會進行任何操作,它只會待在那兒禮貌地發呆。

在 Python 中,init 方法是對象的初始化方法——當我們嘗試創建一個新的 Brain 實例時,它會被調用。通常你需要在編寫新類時自己實現init,所以如果你之前看過 Python 代碼,那init 可能看起來就比較熟悉了,本文就不再贅述。

相比之下,大多數用戶很少明確地實現getattr方法。但它控制着 Python 對象行爲的一個非常重要的部分。具體來說,當用戶試圖通過點語法(如 brain.owner)訪問類屬性,同時這個屬性實際上並不存在時,getattr方法將會被調用。此方法的默認操作僅是引發一個錯誤:

重要的是,我們不用忍受這種行爲。假設我們想創建一個替代接口用於通過以「get」開頭的 getter 方法從 Brain 類的內部檢索數據(這是許多其他語言中的常見做法),我們當然可以通過名字(如 get_owner、get_age 等)顯式地實現 getter 方法。但假設我們很懶,並且不想爲每個屬性編寫一個顯式的 getter。此外,我們可能想要爲已經創建的 Brains 類添加新的屬性(如,brain.foo = 4),在這種情況下,我們不需要提前爲那些未知屬性創建 getter 方法(請注意,在現實世界中,這些是爲什麼我們接下來要這麼做的可怕理由;當然這裏完全是爲了舉例說明)。我們可以做的是,當用戶請求任意屬性時,通過指示 Brain 類的操作去改變它的行爲。

在上面的代碼片段中,我們的 getattr 實現首先檢查了傳入屬性的名稱。如果名稱以 get_ 開頭,我們將檢查對象內是否存在期望屬性的名稱。如果確實存在,則返回該對象。否則,我們會引發錯誤的默認操作。這讓我們可以做一些看似瘋狂的事,比如:

其他不可思議的方法允許你動態地控制對象行爲的其他各種方面,而這在其他許多語言中你沒法做到。事實上,因爲 Python 中的一切都是對象,甚至數學運算符實際上也是對對象的祕密方法調用。例如,當你用 Python 編寫表達式 4 + 5 時,你實際上是在整數對象 4 上調用 add,其參數爲 5。如果我們願意(並且我們應該小心謹慎地行使這項權利!),我們能做的是創建新的特定領域的「迷你語言」,爲通用運算符注入全新的語義。

舉個簡單的例子,我們來實現一個表示單一 Nifti 容積的新類。我們將依靠繼承來實現大部分工作;只需從 nibabel 包中繼承 NiftierImage 類。我們要做的就是定義 and 和 or 方法,它們分別映射到 & 和 | 運算符。看看在執行以下幾個單元前你是否搞懂了這段代碼的作用(可能你需要安裝一些包,如 nibabel 和 nilearn)。

Python 社區

我在這裏提到的 Python 的最後一個特徵就是它優秀的社區。當然,每種主要的編程語言都有一個大型的社區致力於該語言的開發、應用和推廣;關鍵是社區內的人是誰。一般來說,圍繞編程語言的社區更能反映用戶的興趣和專業基礎。對於像 R 和 Matlab 這樣相對特定領域的語言來說,這意味着爲語言貢獻新工具的人中很大一部分不是軟件開發人員,更可能是統計學家、工程師和科學家等等。當然,統計學家和工程師沒什麼不好。例如,與其他語言相比,統計學家較多的 R 生態系統的優勢之一就是 R 具有一系列統計軟件包。

然而,由統計或科學背景用戶所主導的社區存在缺點,即這些用戶通常未受過軟件開發方面的訓練。因此,他們編寫的代碼質量往往比較低(從軟件的角度看)。專業的軟件工程師普遍採用的最佳實踐和習慣在這種未經培訓的社區中並不出衆。例如,CRAN 提供的許多 R 包缺少類似自動化測試的東西——除了最小的 Python 軟件包之外,這幾乎是聞所未聞的。另外在風格上,R 和 Matlab 程序員編寫的代碼往往在人與人之間的一致性方面要低一些。結果是,在其他條件相同的情況下,用 Python 編寫軟件往往比用 R 編寫的代碼具備更高的穩健性。雖然 Python 的這種優勢無疑與語言本身的內在特徵無關(一個人可以使用任何語言(包括 R、Matlab 等)編寫出極高質量的代碼),但仍然存在這樣的情況,強調共同慣例和最佳實踐規範的開發人員社區往往會使大家編寫出更清晰、更規範、更高質量的代碼。

最後小編還爲大家準備了一些Python以及其他編程的學習教程(免費),有想學習編程的小夥伴可以點擊“資料”領取哦,每天拿出2-3個小時自學就可以,學的時間長了,也一下子消化不了,如果你想學習的話,不如就從現在開始學習編程語言吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章