|第2章|
Python for Scientists, Second Edition
IPython入門
IPython字面上很像一款Apple?公司開發的軟體,但實際上它是一個強大的Python解釋器。它由科學家們設計和編寫,目的是以最少的鍵入工作來完成快速的代碼探索和構造任務,并在需要時提供适當(甚至最多)的螢幕線上幫助。有關文檔等的更多資訊請參見對應網站。本章簡要介紹IPython的基本使用要點。更加詳細的描述請參見其他書籍,例如Rossant(2015)。
在本章中,我們将集中讨論IPython的筆記本(notebook)模式和終端(terminal)模式,并且假設讀者已經建構了A.2節和A.3節中所描述的環境。在我們開始實際示例之前,請心急的讀者先忍耐片刻。2.1節讨論的Tab鍵代碼自動補全功能是最大程度減少按鍵次數的非同尋常但有效的方法,2.2節讨論的自省特性将展示如何快速生成相關内聯資訊,而不需要停下來查閱手冊。
2.1 Tab鍵代碼自動補全功能
在使用IPython解釋器時,任何時候都可以使用Tab鍵代碼自動補全功能。這意味着,無論何時開始在指令行或者單元格中鍵入與Python相關的名稱,我們都可以暫停并按下Tab鍵,以檢視在此上下文中與已經鍵入的字元相一緻的可用名稱清單。
例如,假設我們需要鍵入import matplotlib。鍵入i然後按Tab鍵将顯示15個可用的代碼自動補全項。通過觀察,發現其中隻有一個的第2個字母為m,是以再鍵入m然後按Tab鍵将完成import關鍵字的輸入。将此擴充為import m然後按Tab鍵,将顯示30種清單選項,通過觀察,我們需要通過鍵入import matp然後按Tab鍵來完成所需代碼行的輸入。
上述例子顯得有些做作。但是存在一個使用Tab鍵代碼自動補全功能的更加迫切的原因。在開發代碼時,我們傾向于為變量、函數等使用短名稱(為了偷懶)。(在Fortran的早期版本中,名稱的确被限制為6個或者8個字元,但現在長度可以是任意的。)短名稱通常意義不明确,其潛在的危險是當6個月後我們重新檢視代碼時,代碼的意圖可能不再顯而易見。通過使用任意長度的有意義的名稱,我們可以避免這個陷阱。而且由于Tab鍵代碼自動補全功能,整個長名稱的輸入也隻需一次按鍵。
2.2 自省
IPython能夠檢查幾乎任何Python構造(包括其自身),并為開發人員提供其選擇的任何可用資訊報告。該功能被稱為自省(introspection)。它是由單個字元(問号,即“?”)進行通路的。了解自省最簡單的方法是使用這項功能,是以建議讀者打開IPython解釋器。
讀者應該使用哪種模式呢?終端模式還是筆記本模式?初學者應該從終端模式開始(參見A.3節中的描述),以避免過高的複雜度。直到2.5節,我們将一直采用這種方式,因為其中涉及的代碼段非常短。如果使用者選擇使用終端模式,則在指令行視窗中鍵入ipython,然後按Enter鍵。IPython将予以響應并顯示一長段标題,接着顯示以“In [1]:”标注開始的使用者輸入行。接下來就是IPython解釋器環境,讀者可以嘗試鍵入自省功能:在輸入行鍵入?,然後按Enter鍵。(注意,在IPython終端模式下,按Enter鍵意味着實際執行目前行中的指令。)IPython以頁面方式予以響應并顯示所有可用功能一覽。退出這個指令後,鍵入指令quickref(提示:可以使用Tab鍵代碼自動補全功能)将顯示一個更簡潔的版本。強烈推薦讀者仔細研究上述顯示的兩份幫助文檔内容。
IPython筆記本使用者則需要使用稍微不同的指令。在調用筆記本程式(詳細内容請參閱A.2.2節)之後,呈現在使用者面前的是一個未編号的單行空白單元格。當使用者嘗試在輸入行中鍵入自省字元?後,此時按Enter鍵将僅僅在單元格中增加一個新的行。為了執行單元格中的指令,則需要同時按Shift+Enter鍵(執行指令,同時在目前單元格下面建立一個新的單元格),或者同時按Ctrl+Enter鍵(僅僅執行指令)。而輸出(長長的功能一覽)則出現在螢幕底部的可滾動視窗中,通過點選右上角的x按鈕可以關閉該視窗。鍵入指令quickref(提示:可以使用Tab鍵代碼自動補全功能),然後同時按Ctrl+Enter鍵将顯示一個更簡潔的版本。再次強烈推薦讀者仔細研究上述顯示的兩份幫助文檔内容。
然而,科學家們往往是時間寶貴的群體,本章的目的是幫助他們開始使用最有用的特征。是以,我們需要輸入一些Python代碼,新手必須信任這些代碼,直到他們掌握了第3章和第4章的内容。同樣,對于筆記本或者控制台模式,操作過程略有不同。
使用IPython筆記本的使用者應該将每個代碼框中的代碼行或者片段鍵入到一個單元格中。然後可以通過按Ctrl+Enter鍵或者Shift+Enter鍵執行代碼。使用終端模式的讀者應該逐行地輸入代碼中的代碼行或者代碼片段,并通過按Enter鍵來完成每一行的輸入。
例如,請鍵入如下代碼:
a=3
b=2.7
c=12 + 5j
s='Hello World!'
L=[a, b, c, s, 77.77]
前兩行表示a引用一個整數,b引用一個浮點數。Python使用工程師的約定:(數學家可能更喜歡)。然後c引用一個複數。在第5行中分别鍵入a、b和c,将依次執行并顯示辨別符引用的對象的值。注意,顯示多個值有一個有用的快捷方式—在單行上嘗試鍵入a, b, c。接下來嘗試在單行中鍵入c?,結果表明c确實指向一個複數。建立複數的另一種方式是c=complex(12, 5)。接下來嘗試鍵入c.,然後按Tab鍵,解釋器将立即提供三種可能的代碼補全選項。它們代表什麼意思呢?這裡幾乎是顯而易見的,請先嘗試鍵入c.real?。(請使用Tab鍵代碼自動補全功能,而不需要鍵入eal。)結果顯示c.real的值為浮點數12,即複數的實部。初學者可能會嘗試c.imag。接下來請嘗試鍵入c.conjugate?。(同樣隻需要5次按鍵加Enter鍵!)結果表明c.conjugate是一個函數,使用方法如下:cc = c.conjugate()。
這種表述方式對Fortran或者C語言的使用者而言也許有些奇特,他們可能期望類似real(c)或conjugate(c)的方式。文法的改變是因為Python是面向對象的語言。這裡的對象是12 +5j,引用為變量c。是以c.real是關于對象元件的值的查詢,它不會改變對象。然而,c.conjugate()則需要改變對象或者(此處)建立一個新的對象,是以它是一個函數。該表述對于所有對象都是一緻的,更詳細的讨論請參見3.10節。
傳回到上一個代碼片段,在一行中單獨鍵入s,将輸出一個字元串。我們可以鍵入s?和s.來确認,随後按Tab鍵将顯示38種與字元串對象相關的代碼自動補全選項。讀者應該嘗試使用自省方法來發現其中一些選項的具體功能。同樣,鍵入L.?将顯示L是一個清單對象,包括9個代碼自動補全選項。一定要試一試!一般而言,在Python代碼中的任何地方,使用自省和Tab鍵自動代碼補全可以生成相關的幫助文檔。還有進一步的自省指令??(雙英文問号),用于在适當的情況下顯示函數的源代碼,稍後将在2.5節給出一個示例。(我們迄今為止涉及的對象函數都是内置函數,而内置函數不是使用Python語言編寫的!)
2.3 曆史指令
如果讀者觀察上一節中代碼的輸出結果,則會發現IPython采用了一種與Mathematica 筆記本非常類似的曆史指令(history command)機制。輸入行标記為In[1],In[2],…,如果輸入行In[n]産生了任何輸出,則将其标記為Out[n]。為了友善起見,前三個輸入行/單元格可以通過變量名_i、_ii和_iii引用,而相應的輸出行/單元格則可以通過_、__和___引用。但是,在實際操作中,可以通過使用鍵盤上的上方向鍵↑(或者Ctrl-p)和下方向鍵↓(或者Ctrl-n)導航來将前一個輸入行/單元格的内容插入目前輸入行/單元格中,這可能是最常見的使用方法。在終端模式中儲存曆史指令資訊雖然非同尋常,但卻友善使用。如果關閉IPython(使用exit指令)後又重新啟動它,則上一個會話的曆史指令記錄仍然可以通過鍵盤上的上下方向鍵獲得。有關曆史指令機制,有很多更精細的方式,請讀者嘗試鍵入指令history?以獲得相關幫助資訊。
2.4 魔法指令
IPython解釋器希望接收有效的Python指令。同時,還能夠提供一些輸入指令以控制IPython行為或底層作業系統行為。這種與Python共存的指令被稱為魔法指令(magic command)。通過在解釋器中鍵入指令%magic,可以顯示一段很長的詳細說明。通過鍵入指令%lsmagic,可以顯示可用的指令簡要清單。(别忘了使用Tab鍵代碼自動補全功能!)請注意,存在兩種類型的魔法指令:行魔法指令(字首為%)和單元格魔法指令(字首為%%)。單元格魔法指令僅與筆記本模式有關。通過使用自省功能,讀者可以擷取每個魔法指令的幫助文檔資訊。
首先讓我們讨論作業系統指令。一個簡單的示例是pwd,它來自UNIX作業系統,僅僅用于輸出目前目錄(輸出工作目錄)的名稱并退出。在IPython視窗中通常有三種方法來實作該功能。請讀者嘗試如下指令。
!pwd
Python中沒有任何以“!”開始的指令,并且IPython将此解釋為UNIX shell指令pwd,并生成相應的ASCII文本輸出結果。
%pwd
Python中沒有任何以“%”開始的指令,IPython視此為行魔法指令,并将其解釋為shell指令。結果字元串中的u表明結果使用Unicode編碼,進而實作了豐富多樣的輸出。A.3.1節中将簡要涉及Unicode的概念。
pwd
這是一個微妙但有用的特征。行魔法指令并不總是需要字首%,請參見下文有關%auto-magic的讨論。由于解釋器沒有發現pwd的其他定義,是以将其作為魔法指令%pwd來處理。
pwd='hoho'
此時,pwd被指派為一個字元串,是以不會作為魔法指令來處理。
帶%的魔法指令可以消除二義性。
del pwd
使用del删除變量pwd後,此時的pwd沒有其他指派,其作用又恢複為一個行魔法指令。
%automagic?
當%automagic設定為on時(預設值),所有行魔法指令前面的單個百分号(%)都可以省略。這帶來了巨大的便捷性,但讀者必須清醒地意識到自己正在使用魔法指令。
魔法單元格指令以雙百分号(%%)開始,作用于整個單元格,功能十分強大,具體請參見下一節。
2.5 IPython實踐:擴充示例
在本章的剩餘部分,我們會呈現一個擴充示例的第一部分,以展示魔術指令的有效性。我們将在3.9節中讨論該擴充示例的第二部分内容,讨論如何通過分數實作任意精度的實數算術運算。關于分數有一個特點,如3/7和24/56通常被視為相同的分數。是以,這裡存在一個問題,即确定兩個整數a和b的“最大公因子”,或者正如數學家常用的稱謂GCD,其可用于把分數a/b化簡為規範形式。(通過檢查因子,發現24和56的GCD為8,這意味着24/56=3/7,并且不可能進一步化簡。)實作檢查因子的自動化并不容易,一些研究表明可以通過歐幾裡得算法(Euclid’s algorithm)實作。為了簡潔地表達該算法,我們需要一些專業術語。假設a和b是整數。求a整除b,其餘數為a mod b,如13 mod 5 = 3,5 mod 13 = 5。如果用gcd(a, b)表示a和b的最大公因子GCD,則歐幾裡得算法可以通過遞歸算法簡單地描述如下:

請讀者嘗試基于上述算法手工求解gcd(56, 24)。其實很簡單!可以看出,當a和b是連續的斐波那契(Fibonacci)數時,演算會出現最費力的情況,是以它們對測試用例很有用。斐波那契數列Fn通過遞歸算法定義如下:
斐波那契數列為:0,1,1,2,3,5,8,…
如何在Python語言中快速有效地實作歐幾裡得算法和斐波那契數列呢?我們從計算斐波那契數列任務開始,因為它看起來更簡單。
為了開始掌握IPython,希望初學者先充分信任如下兩個代碼片段。這裡僅提供了部分解釋,但在第3章中将對所有的特征進行更全面的解釋。然而,這裡需要強調的重點是,每種程式設計語言都需要輔助代碼塊,例如函數或者do循環的内容。大多數語言都以某種形式的括号來區分代碼塊,但Python隻依賴縮進。所有的代碼塊必須具有相同的縮進。通常使用冒号(:)來表示需要子代碼塊。子代碼塊則進一步縮進(按慣例使用4個空格),并且使用取消額外縮進的方式訓示子代碼塊的結束。IPython及所有支援Python語言的編輯器都會自動處理這個問題。下面給出三個示例。
在我們繼續讨論執行代碼片段的工作流之前,初學者應該嘗試了解(也許是大緻了解)正在發生的事情。
1 # File: fib.py Fibonacci numbers
2
3 """ Module fib: Fibonacci numbers.
4 Contains one function fib(n).
5 """
6
7 def fib(n):
8 """ Returns n'th Fibonacci number. """
9 a,b=0,1
10 for i in range(n):
11 a,b=b,a+b
12 return a
13
14 ####################################################
15 if __name__ == "__main__":
16 for i in range(1001):
17 print "fib(",i,") = ",fib(i)
Python文法的細節将在第3章解釋。目前暫時隻需要了解以#符号開始的行(如第1行和第14行)表示注釋。另外第3~5行定義了一個文檔字元串,其作用将在稍後進行解釋。第7~12行定義了一個Python函數。注意前述内容表明每個冒号(:)都要求縮進代碼塊。第7行是函數聲明。第8行是函數文檔字元串資訊(同樣将在稍後闡述)。第9行引入了辨別符a和b,它們是該函數的局部變量,初始值分别引用值0和1。接下來檢查第11行,暫時忽略其縮進。此處a被設定為引用b最初引用的值,同時b被設定為引用最初a和b引用的值的和。很明顯,第9行和第11行重複計算蘊含在式(2-2)中的公式。第10行引入了一個for循環(或者do循環,這将在3.7.1節闡述),循環包括第11行。此處range(n)生成了一個包含n個元素的清單[0, 1, …, n-1],是以第11行将被執行n次。最後,第12行退出函數,并且傳回最終a引用的值。
當然,我們需要提供一個測試集來證明這個函數是按照預期的方式運作的。第14行僅僅是注釋。第15行将随後解釋。(在輸入時,請注意有四對下劃線。)因為它是一個以冒号結尾的if語句,是以後面的所有行都需要縮進。我們已經了解到第16行代碼的思想。我們使用i=0,1,2,…,1000重複執行第17行代碼1001次。它輸出表示i的值的字元串(包含4個字元),以及表示fib(i)的值的另一個字元串(包含4個字元)。
接下來,我們展示建立和使用上述代碼片段的兩種可能的工作流程。
2.5.1 使用IPython終端的工作流程
首先使用者使用喜歡的編輯器,在運作IPython的目錄中建立一個檔案fib.py,輸入上述代碼片段的内容并儲存。然後,在IPython視窗中運作魔法指令run fib。如果使用者所建立的檔案内容沒有文法錯誤,則運作結果為1001行斐波那契數。如果檔案内容存在文法錯誤,則将輸出其中的第一處錯誤。重新回到編輯器視窗,更正并儲存源代碼。然後再次嘗試運作魔法指令run fib。(初學者可能需要重複數次,但這并不複雜!)
2.5.2 使用IPython筆記本的工作流程
打開一個新的單元格并在其中輸入上述代碼片段,為了穩妥起見請儲存筆記本(使用快捷鍵按ESC鍵後再按s鍵)。接着運作單元格(使用快捷鍵Ctrl+Enter鍵)。如果使用者所輸入的代碼沒有文法錯誤,則運作結果為1001行斐波那契數。如果輸入的代碼存在文法錯誤,則将輸出其中的第一處錯誤。重新回到單元格,修改錯誤代碼,然後再次運作單元格。(初學者可能需要重複數次,但這并不複雜!)一旦程式滿足要求,重新回到單元格,并在最前面插入如下單元格魔法指令:
%%writefile fib.py
現在重新運作該單元格,則單元格中的内容将寫入到目前目錄的fib.py中,如果該檔案已經存在,則覆寫其内容。
一旦程式驗證并運作正确後,我們如何測量其運作速度呢?再次運作程式,但使用增強魔法指令run -t fib,則IPython将産生時間度量資料。在我自己的計算機上,“使用者時間”(User time)是0.05秒,但是“系統時間”(Wall time)是0.6秒。很顯然,該差異反映了有大量字元輸出到螢幕上。要驗證這一點,請将代碼片段修改如下:在第17行的前面插入一個#符号以注釋掉列印輸出語句;添加新的第18行,輸入fib(i),請注意縮進格式正确。(這将對函數進行求值,但不處理計算結果值。)接下來再次運作程式,在我的計算機上耗時0.03秒,這表明fib(i)運作速度極快,但列印輸出速度緩慢。(最後别忘了注釋掉第18行,并取消第17行的注釋!)
接下來我們将解釋第3~5行以及第8行中的文檔字元串,以及奇特的第15行。請關閉IPython(在終端模式下,使用指令exit),然後重新打開一個IPython的會話。僅輸入一行語句import fib,注意隻需要檔案主名,不需要輸入檔案字尾.py。該語句導入了一個對象fib。那麼fib是什麼呢?使用自省指令fib?,IPython将輸出代碼片段中第3~5行的文檔字元串資訊。這表明我們也可以獲得有關函數fib.fib的更多資訊,是以請嘗試指令fib.fib?,結果将傳回第8行中的函數文檔字元串資訊。文檔字元串(docstring)是用三引号括起來的資訊,用途是向其他使用者提供線上幫助文檔。另外,自省還有一個有用的技巧,請嘗試fib.fib??,将輸出該函數的源代碼清單。
讀者應該注意到,運作import fib并沒有輸出前1001個斐波那契數。但是,如果我們在一個單獨的IPython會話中運作指令run fib,則将輸出前1001個斐波那契數!代碼片段的第15行檢測檔案fib.py是被導入還是被運作,并相應地不運作或者運作測試集。其實作原理将在3.4節闡述。
接下來我們繼續讨論原來的任務,即實作公式(2-1)中的gcd函數。一旦我們認識到Python實作遞歸沒有任何問題,并且a mod b被實作為a%b,那麼最簡單的解決方案可以通過如下的代碼片段實作(請暫時忽略第14~18行)。
1 # File gcd.py Implementing the GCD Euclidean algorithm.
2
3 """ Module gcd: contains two implementations of the Euclid
4 GCD algorithm, gcdr and gcd.
5 """
6
7 def gcdr(a,b):
8 """ Euclidean algorithm, recursive vers., returns GCD. """
9 if b==0:
10 return a
11 else:
12 return gcdr(b,a%b)
13
14 def gcd(a,b):
15 """ Euclidean algorithm, non-recursive vers., returns GCD. """
16 while b:
17 a,b=b,a%b
18 return a
19
20 ##################################################
21 if __name__ == "__main__":
22 import fib
23
24 for i in range(963):
25 print i, ' ', gcd(fib.fib(i),fib.fib(i+1))
上述代碼片段中唯一真正的新内容是第22行中的import fib語句,而上文我們已經讨論了其作用。第24行和第25行中循環的執行次數十分關鍵。輸出結果表明,上述代碼片段運作耗時為幾分之一秒。接下來将第24行中的參數963更改為964,儲存檔案,并再次執行run gcd。結果發現輸出是一個無限循環,稍微耐心等待後,最終程序會終止,并顯示錯誤資訊“the maximum recursion depth has been exceeded”(超過最大遞歸深度)。雖然Python允許遞歸,但對自調用的數量存在限制。
這個限制對使用者而言可能不是大問題。但有必要花點時間考慮是否可以在不使用遞歸的情況下實作歐幾裡得算法(2-1)。我提供了一個解決方案,位于代碼片段第14~18行實作的函數gcd中。第16行和第17行定義一個while循環,請注意第16行後面的冒号。在while和冒号之間,Python期望一個計算結果為布爾值(True或者False)的條件表達式。隻要條件表達式的計算結果為True,則循環執行第17行,然後重新測試條件表達式。如果測試結果為False,則循環終止,控制轉移到下一語句(第18行)。在該上下文中,b是一個整數,那麼整數值如何轉換為布爾值呢?答案非常簡單:整數值0總是被強制轉換為False;所有非零值則被強制轉換為True。是以,當b變為0時,則循環結束,然後函數傳回值a。這是公式(2-1)的第一個子句。第17行的轉換是第二個子句,是以這個函數實作了歐幾裡得算法。它比遞歸函數短,可以被調用任意次數,并且我們将看到,其運作速度更快。
那麼,上述改進算法是否有效呢?使用run指令,我們可以獲得統計資料。首先,編輯代碼片段,修改代碼使函數gcdr循環運作963次,并儲存代碼。接下來執行run -t gcd,以獲得運作耗時。在我的計算機上,“使用者時間”是0.31秒(每個讀者計算機上的耗時可能會不同,但主要考慮的是相對時間。“系統時間”則反映輸出顯示時間開銷,并且與此處無關。接下來執行run -p gcd以調用Python性能分析器。雖然了解輸出顯示結果的每個方面需要閱讀相關幫助文檔資訊,但也可以使用科學直覺感覺。結果表明,在總共464 167次實際調用中,有963次直接調用(與預期相符)函數gcdr。該函數實際耗時0.237秒。其次,函數fib被調用了1926次(與預期相符),并且耗時0.084秒。注意,這些時間不能與前面run -t gcd的運作結果相比較,因為run -p gcd的耗時包括性能分析器的時間開銷(并且此處不能忽略)。然而,我們可以得出結論,函數gcdr耗費了大約74%的時間開銷。
接下來我們重複有關函數gcd的練習。修改代碼段的第25行,用gcd替換gcdr并儲存檔案。接下來執行run -t gcd,結果使用者時間是0.20秒。執行另一個指令run -p gcd,結果顯示函數fib被調用了1926次,耗時0.090秒。但是,函數gcd隻被調用了993次(與預期相符),耗時為0.087秒。是以,gcd耗時占比為49%。近似地,相對耗時可以消除性能分析器時間開銷的影響。結果表明,遞歸版本0.31秒耗時的74%是0.23秒,而非遞歸版本0.20秒耗時的49%是0.098秒。是以,改進算法思想的結果代碼更加短小,運作時間是原始算法的43%!
從上述示例中我們可以總結如下兩點:
1.IPython的魔法指令run或者%run可以提高Python的工作效率。讀者可以通過自省指令來查閱其文檔字元串。也請注意%run -t和%run -p的差別。同時,建議讀者嘗試自省指令%timeit。
2.讀者在文獻中會查閱到許多關于“加速”Python的方法,這些方法通常是非常聰明的軟體工程方法。但沒有一種能像人類的創造力那樣有效!