來源: ApacheCN『USF MSDS501 計算數據科學中文講義』翻譯項目原文:How to read codepython
譯者:飛龍c++
協議:CC BY-NC-SA 4.0git
從根本上說,程序員與代碼交流。咱們不只向計算機,也向其餘開發人員表達了咱們的想法。到目前爲止,咱們專一於設計程序和編寫 Python 代碼。這是關鍵的創做過程,可是,爲了編寫代碼,程序員必須可以閱讀其餘人編寫的代碼。程序員
咱們閱讀代碼以便:github
<img src="https://gitee.com/wizardforce...; width="30" align="left">算法
在咱們討論庫函數時,讓我強調一條黃金法則:你永遠不該該向你的程序員詢問參數的細節和庫函數的返回值。你能夠經過 PyCharm 中的「跳轉到定義」或網絡搜索來本身發現它。apache
本文檔的目的是解釋程序員如何讀取代碼。 咱們的第一個線索來自於咱們不是計算機這一事實,所以,咱們不該該像計算機同樣閱讀代碼,一個接一個地檢查一個符號。 相反,咱們將尋找關鍵元素和代碼模式。編程
這就是咱們用外語閱讀句子時所作的事情。 例如,個人法語很是糟糕,所以,在閱讀法語句子時,我必須有意識地詢問誰在對誰作什麼。在實踐中,這意味着識別主語,動詞和賓語。從這些關鍵要素中,我試圖想象做者心中的思惟模式。基本上我試圖反轉做者所遵循的過程。數組
在編程世界中,過程以下:代碼做者可能會想到「經過除以 2 將價格轉換爲新列表」,而後將它們轉換爲「映射」的僞代碼,最後轉換爲 Python for
循環。在閱讀循環代碼時,咱們的工做是反轉過程,並想象做者的原始目標。 咱們不是試圖經過在咱們的頭腦或紙上模擬它,來弄清楚代碼的突現行爲;相反,咱們正在尋找模式,它們可以告訴咱們正在執行哪些高級操做。網絡
這就是爲何在編寫代碼時應該強調清晰度,以便讀者閱讀更多內容。約翰 F. 伍茲 有一個很好的引言,總結了不少東西:
寫代碼的時候老是想象,維護你代碼的傢伙是一個知道你住在哪裏的暴力精神病患者。
在第一次查看教科書時,掃描目錄來得到書籍內容的總體視圖,是有意義的。 第一次看節目時也是如此。 查看全部文件以及這些文件中包含的函數的名稱。 同時,找出主程序的位置。 根據您在程序中的目標,您可能會開始單步執行主程序或當即跳轉到感興趣的函數。
從樣例運行或單元測試中查看程序的輸入 - 輸出對也頗有用,由於它能夠幫助您瞭解程序的功能。 從某種意義上說,咱們經過檢查和測試程序,對程序的工做計劃進行逆向工程。 之前,咱們在前進方向使用程序的工做計劃來設計程序。
一旦咱們肯定了要檢查的主程序或函數,就應該對函數的工做計劃進行反向工程。 函數的名稱多是函數功能的最大線索,假設代碼做者是一個不錯的程序員。 (使用像f
這樣的通用函數名稱,是教師在不泄露答案的狀況下,編寫代碼閱讀問題的方式。)例如,毫無疑問,如下函數的目標是什麼:
def average(...): ...
即便不查看參數或函數語句。
程序員一般會提供函數用法的註釋,但要當心。 程序員一般會在不更改註釋的狀況下更改代碼,所以註釋會產生誤導。可接受的註釋可能以下所示:
def average(...): "Compute and return the average of a list of numbers" ...
若是咱們幸運的話,該註釋對應於工做計劃中的函數目標描述。
下一步是肯定參數和返回值。 一樣,參數的名稱常常告訴咱們不少,但不幸的是,Python 一般沒有明確的參數類型(它們不會被 Python 檢查)因此咱們必須本身解決這個問題。 瞭解值和變量的類型對於理解程序相當重要。 在這樣的簡單函數中,咱們一般能夠快速找出參數的類型和返回值。 在其餘狀況下,咱們將不得不深刻研究函數的語句來解決這個問題(稍後會詳細介紹)。 讓咱們放大來查看咱們函數的更多細節:
def average(data): ... return sum / n
在這一點上,咱們知道data
幾乎確定是一個數字列表,函數返回一個數字。 這意味着咱們能夠填寫該功能的工做計劃的第一部分。
由於咱們事先知道平均值是什麼,因此咱們能夠填寫函數目標的工做計劃描述。 可是,通常來講,咱們必須掃描函數的語句才能弄明白。 (咱們可能會很幸運並找到合理的函數註釋。)如今讓咱們看一下完整的函數:
def average(data): n = len(data) sum = 0.0 for x in data: sum = sum + x return sum / n
缺少經驗的程序員必須單獨和逐字地檢查函數的語句,模擬計算機來找出突現行爲。 相比之下,經驗豐富的程序員在代碼中尋找模式,表明映射,搜索,過濾等高級操做的實現.....
經過類比,考慮在遊戲過程當中記住棋盤的狀態。 初學者必須單獨記住全部東西在哪兒,而國際象棋大師則認爲棋盤只是布達佩斯開局的變種。
咱們如何知道從哪裏開始以及看什麼? 那麼,讓咱們回想一下咱們的通用數據科學程序模板:
該過程的要點是,將數據加載到方便的數據結構中並對其進行處理。加載數據,建立數據結構和處理數據結構有什麼共同之處?它們都重複執行一組操做,這意味着處理數據的程序的要點是循環。(甚至有一本着名的書名爲算法+數據結構=程序,其中算法表示僞代碼或代碼描述的過程。)沒有循環的程序可能會很是無聊,由於它沒法遍歷數據結構或處理數據文件。
從這裏,咱們能夠得出結論,全部的動做都發生在循環中,因此咱們應該首先在代碼中尋找循環**。閱讀代碼是在函數代碼中找到這樣的模板的問題,它當即告訴咱們做者想要的操做或模式的類型。
讓咱們深刻研究一些循環示例,嘗試識別高級模式和相應的操做。 要尋找的關鍵要素是咱們研究的模板中的空位。 這一般意味着識別循環變量,循環邊界,咱們正在遍歷的數據結構以及對數據元素執行的操做。目標是對代碼做者的意圖進行逆向工程。
練習:首先,上面的sum
函數中的代碼模式的對應操做是什麼?
sum = 0.0 for x in data: sum = sum + x
那是一個累積器。
練習:讓咱們看一個循環,我故意使用蹩腳的變量名稱,因此你必須專一於功能。
foo = [] for blah in blort: foo.append(blah * 2)
這是一個映射操做,咱們能夠從空目標列表的初始化和foo.append(...)
調用中看到。 除了目標列表是blah
的函數,它來自源列表blort
以外,blah * 2
與尋找模式無關。
練習:你在下面的代碼中看到了什麼樣的循環(for-each
,索引,嵌套等等)? 代碼執行什麼樣的高級操做?
blort = [] for boo in range(len(foo)): blort.append(foo[boo] * 2)
這是一個索引循環,它再次執行映射操做。 它是一個索引循環的線索是,邊界是range(len(foo))
,它給出一系列索引。 因爲blort.append
和foo[boo]
的引用,咱們知道它是一個映射操做。 由於[boo]
索引運算符,咱們知道foo
是某種類型的列表。
練習:對應此代碼中模式的高級操做是什麼:
foo = [] for i in range(len(X)): foo.append(X[i]+Y[i])
它將兩列(列表)組合成目標列/列表foo
。咱們知道X
和Y
是列表,由於[i]
數組索引。
練習:此代碼執行什麼高級數學運算?
for i in range(n): for j in range(n): C[i][j] = A[i][j] + B[i][j]
矩陣加法。這裏重要的是要認識到,嵌套的索引循環給出了在[0..n]
範圍內的循環變量i
和j
的全部組合。 執行此操做的最多見緣由之一是迭代矩陣或圖像的元素。 這裏的答案也多是圖像加法。
練習:這個循環打印了多少個hi
?
for i in range(n): for j in range(n): print('hi')
n * n
。內循環n
次。外循環意味着咱們執行整個內循環n
次。
練習:
blort = [] for foo in A: for bar in B: blort.append(foo + bar)
這從A
和B
的全部可能組合中找到全部狀況。
練習:這段代碼在作什麼? 即,循環完成後,blort
的值是多少?
blort = float('-inf') for x in X: if x > blort: blort = x print(blort)
X
的最大值。
<img src="https://gitee.com/wizardforce...; width="30" align="left">
不管什麼時候在循環內部看到if
語句,請考慮過濾或搜索或條件累積。 它一般是其中一個的變體。這假設條件表達式是直接或間接的循環變量的函數。
練習:這個變體打印了什麼?
blort = float('-inf') for i in range(len(X)): if X[i] > blort: blort = X[i] print blort
徹底同樣的東西; blort
是X
的最大值。 您會看到一個條件表達式,它是循環內部循環變量的函數。這只是前一次的重組。
練習:這段代碼的目標是什麼? 即,循環後它爲foo
打印的值是多少?
foo = -1 bar = -99999 for i in range(len(X)): if X[i] > bar: bar = x foo = i print(foo)
X
的最大值索引(argmax
)。 咱們知道與條件相關的代碼,是從前面的例子中找出最大值,但它也跟蹤了索引i
。
能夠把它想象成你已經想到的標準模式,但這種變體能夠作一些額外的事情。 而後問二者之間有什麼區別。
這是嘗試理解輸入 - 輸出對是什麼的一個很好的例子(雖然咱們在這裏談論的是代碼段而不是完整的函數)。 在最大值的計算中,輸出是取自X
的值。 在這種狀況下,打印出的值是0..len(X)-1
中的索引。
練習:描述此代碼完成後bar
的值。
foo = [] bar = [] for blah in blort: foo.append(blah * 2) for zoo in foo: if zoo>10: bar.append(zoo)
這裏有不少東西,但它實際上只不過是一個序列中的兩個模式。 第一個模式是一個映射操做,它將blort
中的值加倍,來建立foo
列表,該列表由第二個循環使用。第二個循環只是一個過濾,它將全部> 10
的值從foo
提取到bar
。
練習:執行此代碼後,a
和b
是什麼值?
a = 0 b = 0 for x in X: if x < 10: a = a + 1 else: b = b + 1
這是一個具備條件的雙重累積。 它是一個累積,由於它在循環中更新至少一個變量。 它有一個累積條件,由於它是一個累積循環中的條件,其中條件表達式測試循環迭代器的值。 a
是X
中小於 10 的值的數量,b
是大於或等於 10 的值的數量。
練習:循環後Y
是什麼值?
a = 2 b = 5 Y = [] for i in range(len(X)): if i>=a and i<=b: Y.append(X[i])
此循環實現切片操做,從列表中提取元素的子集。 在這種狀況下,它選擇範圍a..b
中的X
的元素,包含邊界,並將它們添加到Y
。
該實現效率很是低,由於它遍歷整個列表來獲取範圍內的元素。 若是咱們將循環的邊界更改成所需範圍,它會更快更容易理解:
a = 2 b = 5 Y = [] for i in range(a, b+1): # range is [a,b] Y.append(X[i])
編寫代碼是成爲程序員的重要部分。代碼是程序員的溝通方式。 這是咱們有效地使用額外的庫,調試,以及得到經驗的方式。
閱讀代碼的技巧是翻轉從函數工做計劃到僞代碼再到 Python 代碼的編程過程。 最大的線索來自變量和函數名稱,可能還有代碼註釋。 而後,咱們查找代碼中所表達的代碼模板,根據該模板的選擇,對做者的原始意圖進行反向工程。 例如,詢問代碼表明映射仍是搜索操做。 不要試圖模仿計算機,並使用表達式值和時間的圖表來猜想突現行爲。 有時你必須這樣作才能進行調試,但總的來講,你的目標是猜想代碼做者的意圖。
由於閱讀代碼是您流程的重要組成部分,因此經過編寫高質量的代碼,善待其餘開發人員和您將來的自已。 這包括選擇優秀的變量和函數名稱以及編寫清楚說明您意圖的代碼。