|第3章|
Python for Scientists, Second Edition
Python簡明教程
雖然Python是一種小語言,但其涉及的内容卻非常豐富。在編寫教科書時,常常會有一種按主題全面闡述程式設計語言各個方面的沖動。最明顯的例子是來自Python創始人吉多·範羅蘇姆的入門教程。Python入門教程以電子形式包含在Python文檔中,也可以線上獲得,或者購買紙質版本書籍(Guido van Rossum和Drake Jr.(2011))。這本書相對比較簡潔,隻有150頁的内容,而且沒有涉及NumPy。我最喜歡的教科書是Lutz(2013),該書超過1500頁,是一種馬拉松式學習曲線,而且隻是稍微提及NumPy。該書之是以非常優秀,是因為它提供了各功能的詳細解釋,但對于Python語言的第一門課程而言,其内容太過繁雜。另外兩本教科書Langtangen(2009)和Langtangen(2014)的内容更傾向于科學計算,但都存在同樣的問題,它們都差不多800頁,且内容重疊較多。建議讀者把上述書籍(以及其他書籍)作為參考書,但它們都不是适用于學習Python語言的教科書。
學習一門外語時,很少有人會先學習一本文法教材,然後背字典。大多數人的學習方法是開始學習一些基本文法和少量詞彙,然後通過實踐逐漸擴大其文法結構和詞彙量的範圍。這種學習方法可以使他們快速地了解和使用語言,而這就是本書采用的學習Python的方法。這種學習方法的缺點是文法和詞彙被分散到整個學習過程中,但這個缺點可以通過使用其他教科書來改善,例如前一段中所提到的那些教科書。
3.1 輸入Python代碼
雖然可以僅僅通過閱讀教程内容的方式來學習,但在閱讀的同時使用手頭的IPython終端來嘗試示例代碼會更有幫助。對于較長的代碼片段(如3.9節和3.11節中的代碼片段),則建議使用筆記本模式,或者終端模式與編輯器一起使用,以便儲存代碼。附錄A中的A.2和A.3節描述了這兩種方法。在嘗試了這些代碼片段之後,強烈鼓勵讀者在解釋器中嘗試自己的實驗。
每種程式設計語言都包含代碼塊,代碼塊是由一行或者多行代碼組成的文法體。與其他語言相比,Python基本上不使用圓括号()和大括号{},而是使用縮進作為代碼塊格式化工具。在任何以冒号(:)結尾的行之後,都需要一個代碼塊,代碼塊通過一緻的縮進與周圍的代碼區分開來。雖然沒有指定縮進的空白字元個數,但非官方标準一般使用4個空白字元。IPython和所有支援Python的文本編輯器都會自動完成這種格式化操作。若要還原到原始縮進級别,則請使用Enter鍵輸入一個空行。不使用括号可以提高可讀性,但缺點是代碼塊中的每一行都必須與前面的縮進相同,否則将發生文法錯誤。
Python允許兩種形式的注釋(comment)。一種是#符号,表示目前行中#符号後的其餘部分是注釋。文檔字元串(docstring)可以跨越多行,并且可以包含任何可列印字元。文檔字元串由一對三引号來界定,例如:
""" This is a very short docstring. """
為了完整性,讀者注意到我們可以在同一行上放置多條語句,隻要用分号将它們分開,但是應該考慮可讀性。長語句可以使用續行符号“”來分成多行。并且,如果一個語句包括一對括号(),那麼我們可以在它們之間的任何位置點分行,而不需要使用續行符号“”。一些簡單的示例如下所示:
a=4; b=5.5; c=1.5+2j; d='a'
e=6.0*a-b*b+\
c**(a+b+c)
f=6.0*a-b*b+c**(
a+b+c)
a, b, c, d, e, f
3.2 對象和辨別符
Python包含大量的對象和辨別符。對象可以被認為是一個計算機記憶體區域,包含某種資料以及與資料相關的資訊。對于一個簡單的對象,這些資訊包括它的類型和辨別(即記憶體中的位置,很顯然這與計算機相關)。是以,大多數使用者對對象辨別并不感興趣。使用者需要與機器無關的通路對象的方法,這可以通過辨別符提供。辨別符是附加到對象上的一個标簽,由一個或者多個字元組成。辨別符的第一個字母必須是字母或者下劃線,後續字元必須是數字、字母或者下劃線。辨別符是區分大小寫的:x和X是不同的辨別符。(以下劃線開始或結束的辨別符具有專門用途,是以初學者應該避免使用。)我們必須避免使用預定義的辨別符(如list),并且應該總是嘗試使用有意義的辨別符。然而,選擇xnew、x_new或xNew,則是使用者個人的偏好。請嘗試運作如下代碼片段,建議讀者在終端視窗中逐行鍵入,可以更加明确其含義:
1 p=3.14
2 p
3 q=p
4 p='pi'
5 p
6 q
注意,我們從來沒有聲明辨別符p引用的對象的類型。在C語言中我們必須聲明p為“double”類型,在Fortran中則必須聲明p為“real*8”類型。這不是偶然或者疏忽。Python語言的一個基本特征是類型屬于對象,而不是辨別符。
接下來,在第3行我們設定q=p。右側由p指向的對象替換,q是指向這個對象的新的辨別符(如圖3-1所示)。這裡沒有測試辨別符q和p相等性的含義!注意,在第4行中,我們将辨別符p重新指派為一個“string”對象。但是,辨別符q仍然指向原始的浮點數對象(如圖3-1所示),第5行和第6行的輸出可以證明上述結論。假設我們要重新指派辨別符q。然後,除非中間把q指派給另一個辨別符,否則原始的“float”對象将沒有任何指派的辨別符,是以程式員将無法通路它。Python将自動檢測并釋放計算機記憶體,這個過程被稱為自動垃圾收集。

圖3-1 Python中指派的示意圖。在第一條指令p=3.14執行之後,建立浮點數對象3.14,并将其指派給辨別符p。在這裡,對象被描述為對象辨別(對象辨別是一個大數值,對應于計算機中存儲對象資料的記憶體位址(高度依賴于計算機))和對象類型。第二條指令q=p将辨別符q配置設定給同一個對象。第三條指令p='pi'将p配置設定給一個新的“string”對象,而q指向原始的浮點數對象
指派語句在稍後的章節中十分重要,我們強調指派操作是Python程式的基本建構塊之一,盡管指派語句看起來像是相等性判斷,但它與相等性判斷無關。其文法格式如下:
=
3.3 數值類型
Python包括三種簡單的數值對象類型,我們還将介紹第四種類型(稍微複雜)。
3.3.1 整型
Python語言中整型資料為int。早期版本支援的整型數值範圍為[-231, 231-1],但是新的版本整型數值範圍進一步擴大,現在Python支援的整型數值範圍幾乎沒有限制(僅受計算機記憶體限制)。
整型數值當然支援通常的加法(+)、減法(-)和乘法(*)運算。而對于除法運算則需要稍微注意:即使p和q是整數,p/q的結果也不一定是整數。一般地,我們可以假設q>0(不失一般性),在這種情況下,存在唯一的整數m和n,滿足:
p=mq+n,其中0≤n<q
在Python語言中,整型除法定義為p//q,其結果為m。使用p%q,可以求得餘數n。使用p**q,可以求得乘幂pq,當q<0時,結果為實數。
3.3.2 實數
Python語言中浮點數為float。在大多數安裝環境中,浮點數的精度大約為16位,其數值範圍為(10-308, 10308)。浮點數對應于C語言家族中的double,對應于Fortran家族中的real*8。浮點數常量的标記遵循一般标準,例如:
-3.14, -314e-2, -314.0e-2, -0.00314E3
上述浮點數都表示相同的float值。
常用的加法、減法、乘法、除法和幂的運算規則同樣适用于浮點數運算。對于前三種運算,可以無縫地實作混合模式操作,例如,如果需要求一個int和float之和,則int被自動向上轉換(widening)為float。該規則同樣适用于除法運算(如果一個操作數是int而另一個操作數是float)。然而,當兩個操作數都是int(例如,±1/5)時,結果會是什麼呢?Python的早期版本(<3.0)采用整數除法規則:1/5=0,-1/5=-1,而版本号≥3.0的Python則采用實數除法規則:1/5=0.2,-1/5=-0.2。這是一個潛在的坑,但很容易避免:可以采用整除運算符//,或者向上轉換其中的一個操作數以保證運算結果沒有二義性。
Python語言具有一個繼承于C語言的有用特性。假設我們希望将a引用的浮點數遞增2,很顯然可以使用如下代碼:
temp=a+2
a=temp
雖然上述代碼結果正确,但若使用如下單一指令,則速度更快、效率更高:
a+=2
當然,上述規則同樣适用于其他算術運算符。
可以顯式地将一個int向上轉換到一個float,例如,float(4)将傳回結果4.或者4.0。把一個float向下轉換(narrowing)到一個int的算法如下:如果x是一個正實數,則存在一個整數m和一個浮點數y,滿足:
x=m+y,其中0≤y<1.0
在Python語言中,float向下轉換為int可通過int(x)實作,結果為m。如果x為負數,則int(x)=-int(-x)。該規則可以簡潔地表述為“向0方向截取整數”,例如,int(1.4)=1和int(-1.4)=-1。
在程式設計語言中,我們期望提供大量熟悉的數學函數來用于程式設計。Fortran語言内置了最常用的數學函數,但在C語言家族中,我們需要在程式的頂部通過一個語句(如#include math.h)導入數學函數。Python語言同樣需要導入一個子產品,作為示例我們讨論math子產品,該子產品包括許多用于實數的标準數學函數(子產品将在3.4節中定義)。首先假設我們不知道math子產品包含哪些内容。下面的代碼片段首先加載子產品,然後列出其内容的辨別符。
import math
dir(math) # or math.<TAB> in IPython
要了解有關具體對象的更多資訊,可以查閱書面文檔或者使用内置幫助,例如在IPython中:
math.atan2? # or help(math.atan2)
如果讀者非常熟悉子產品中包含的内容,則可以在調用函數之前,在代碼中的任何地方使用一個快速的解決方案來替換上面的導入指令:
from math import *
随後,上面提到的函數可以直接使用atan2(y,x)的方式,而不是math.atan2(y,x)的方式,乍看起來這非常美妙。然而,還存在另一個子產品cmath,其中包含了許多關于複數的标準數學函數。接下來假設我們重複使用上述快速導入解決方案:
from cmath import *
那麼,atan2(y,x)代表哪一個函數呢?可以把一個實數向上轉換到一個複數,而反過來則不能轉換!注意,與C語言不同,import語句可以位于程式中的任何地方,隻要在需要其内容的代碼之前即可,是以混亂正靜靜地等待着破壞使用者的計算!當然Python也意識到了這個問題,我們将在3.4節中描述推薦的工作流程。
3.3.3 布爾值
為了完整性,這裡我們将讨論布爾值或者bool,它是int的子集,包含兩個可能的值True和False,大緻等同于1和0。
假設變量box和boy引用bool對象值,則表達式(如“not box”“box and boy”和“box or boy”)具有特殊的含義。
int和float類型值(如x、y)定義了标準的相等運算符,例如x==y(等于)、x!=y(不等于)。為了提醒讀者注意Python浮點數的局限性,下面是一個簡單的練習,請猜測以下代碼行的運作結果,然後鍵入、運作該行代碼并解釋其執行結果。
math.tan(math.pi/4.0)==1.0
對于比較運算符“x>y”“x>=y”“xz”等同于:
(0<=x) and (x<1) and (1z)
注意,在上例中x和y或z之間并沒有進行直接比較。
3.3.4 複數
前文已經介紹了三種數值類型,它們構成了最簡單的Python對象類型,它們是更複雜的數值類型的基礎。例如,有理數可以用一對整數來實作。在科學計算中,複數可能是更有用的複雜數值類型,它是使用一對實數來實作的。雖然數學家通常使用i表示,但大多數工程師則傾向于使用j,而Python語言采用了後者。是以,一個Python複數可以顯式地定義為諸如c=1.5–0.4j的形式。請仔細觀察該文法:j(也可以使用大寫的J)緊跟在浮點數的後面,中間沒有包括符号“*”。另一種把一對實數a和b轉換為一個複數的文法是c=complex(a, b)。也可以使用下列文法把上面語句中定義的複數c轉換為實數:c.real傳回實數a;c.imag傳回實數b。另外,文法c.conjugate()将傳回複數c的共轭複數。有關複數屬性的文法将在3.10節詳細讨論。
Python複數支援五種基本的算術運算,并且在混合運算模式中,将自動進行向上數值類型轉換。另外,還包含一個針對複數運算的數學函數庫,這需要導入庫cmath,而不是math。然而,根據顯而易見的原因,複數沒有定義前文描述的涉及排序的比較運算,但可以使用等于運算符以及不等于運算符。
到此為止,讀者已經學習了足夠的Python知識,可以将Python解釋器作為一個複雜的包含五種功能的電腦來使用,強烈建議讀者嘗試一些自己的示例。
3.4 名稱空間和子產品
當Python正在運作時,它需要儲存已配置設定給對象的那些辨別符清單,此清單被稱為名稱空間(namespace),并且作為Python對象,名稱空間也具有辨別符。例如,當在解釋器中工作時,名稱空間具有一個不大好記憶的名稱:__main__。
Python的優勢之一是它能夠包含由讀者或者其他人編寫的檔案(其中包含對象、函數等)。為了實作這種包含其他檔案的功能,假設讀者已經建立了一個包含可以重用的對象(如obj1和obj2)的檔案,并且儲存了檔案(例如,儲存為foo.py,其字尾必須為.py。請注意,對于大多數文本編輯器而言,都要求該檔案字尾為.py,以便支援處理Python代碼)。這種Python檔案被稱為子產品(module)。子產品(foo.py)的辨別符為foo,即其檔案主名,不包括其字尾。
在後續的Python會話中,可以通過下列語句導入該子產品:
import foo
(當首次導入該子產品時,會将其編譯成位元組碼并寫入磁盤檔案foo.pyc。在随後的導入中,解釋器直接加載這個預編譯的位元組碼,除非foo.py的修改日期更近,在這種情況下,将自動生成檔案foo.pyc的新版本。)
上述導入語句的作用是引入子產品的名稱空間foo,随後可以借助如下方法來使用foo中的對象,例如辨別符foo.obj1和foo.obj2。如果能夠确信obj1和obj2不會與目前名稱空間中的辨別符沖突,則也可以通過下列語句直接導入obj1和obj2,然後使用諸如obj1的形式進行直接引用。
from foo import obj1, obj2
使用3.3.2節中的“快速導入”方法,則等同于如下語句:
from foo import *
該語句導入子產品foo名稱空間中的所有對象。如果目前名稱空間中已經存在一個辨別符obj1,則導入過程将覆寫辨別符obj1,通常這意味着原來的對象将無法被通路。例如,假設我們已經有一個引用浮點數的辨別符gamma,則執行如下導入語句:
該導入語句将覆寫原來的辨別符gamma,現在gamma指向cmath庫中的(實數)gamma函數。接下來執行如下導入語句:
該導入語句将把gamma覆寫為(複數)gamma函數!另外要注意,由于import語句可以在Python代碼的任何位置出現,是以使用該導入方法将導緻潛在的混亂。
除非是在解釋器中進行快速的解釋工作,否則最佳實踐方案是修改導入語句,例如:
import math as re
import cmath as co
是以,在上述示例中,可以同時使用gamma、re.gamma和co.gamma。
我們現在已經了解了足夠的背景知識,接下來解釋如下神秘的代碼行:
if name == "__main__"
該語句出現在2.5節中的兩個代碼片段内。第一次出現在檔案fib.py中。現在,如果我們将這個子產品導入解釋器中,那麼它的名稱是fib,而不是__main__,是以這個代碼行後面的代碼行将被忽略。但是,在開發子產品中的函數時,通常直接通過%run指令運作子產品,此時(正如本節開始時所解釋的)子產品内容被讀入__main__名稱空間,是以滿足代碼行中的if條件,繼而将執行随後的代碼。在實踐中,這種方法非常便利。在開發一系列對象(如函數)時,我們可以在附近編寫輔助測試功能代碼。而在生産模式中,通過import語句導入子產品時,這些輔助功能代碼被有效地“注釋”了。
3.5 容器對象
計算機的有用性很大程度上取決于它們能快速執行重複性任務的能力。是以,大多數程式設計語言都提供容器對象,通常稱為數組,它可以存儲大量相同類型的對象,并通過索引機制檢索它們。數學向量将對應于一維數組,而矩陣對應于二維數組。令人驚訝的是,Python核心語言竟然沒有數組概念。相反,它有更加通用的容器對象:清單(list)、元組(tuple)、字元串(string)和字典(dict)。我們很快就會發現,可以通過清單來模拟數組對象,這就是以前在Python中進行數值處理的工作方式。由于清單的通用性,這種模拟方法與Fortran或者C中的等價結構相比要耗費更多的時間,其數值計算緩慢理所當然地為Python帶來了壞名聲。開發人員提出了各種方案來緩解這個問題,現在他們已經提出了标準的解決方案,就是使用NumPy附加子產品(将在第4章闡述)。NumPy的數組具有Python清單的通用性,但其内部實作為C的數組,這顯著地減少(但并非完全消除)了其速度損失。然而,本節将詳細讨論這些核心容器對象,以進行大量的科學計算工作。它們擅長“行政、簿記雜務”,而這正是Fortran和C語言的弱項。用于特大數量的數值處理的數值數組則推遲到下一章,但是對數值特别感興趣的讀者也需要了解本節的内容,因為本節的知識點将延續到下一章。
3.5.1 清單
請讀者在IPython終端輸入并執行如下代碼片段:
1 [1,4.0,'a']
2 u=[1,4.0,'a']
3 v=[3.14,2.78,u,42]
4 v
5 len(v)
6 len? # or help(len)
7 v*2
8 v+u
9 v.append('foo')
10 v
第1行是Python清單的第一個對象執行個體,是包括在方括号中由逗号分隔的Python對象的有序序列。它本身是一個Python對象,可以被指派給一個Python辨別符(例如第2行)。與數組不同,不要求清單的元素都是相同類型。在第3行和第4行中,我們看到在建立清單時,辨別符被它所引用的對象替換,例如,一個清單可以是另一個清單中的元素。初學者應該再次參考圖3-1。注意清單是對象,而不是辨別符。在第5行中,我們調用一個非常有用的Python函數len(),它傳回清單的長度,這裡的傳回結果是4。(Python函數将在3.8節中讨論。同時,我們可以在IPython中通過輸入“len?”來了解len的用途。)我們可以通過類似第7行的構造語句來重複清單内容,第8行的代碼用于拼接清單。我們可以将項目追加到清單的末尾(如第9行)。這裡v.append()是另一個有用的函數,僅适用于清單。讀者可以嘗試v.append或者help(v.append),以檢視其幫助資訊。另外,輸入list.然後按Tab鍵或者執行help(list)指令,将顯示清單對象的内置函數。它們類似于3.3.4節中的c.conjugate()。
3.5.2 清單索引
我們可以通過索引來通路清單u的元素u[i],其中i是一個整數,并且i∈[0, len(u))。請注意,索引從u[0]開始,到u[len(u)-1]結束。到目前為止,這與數組索引通路(例如C或者Fortran語言)十分相似。然而,一個Python清單(例如u)“知道”其長度,是以我們也可以以相反的順序來索引通路元素u[len(u)-k],其中k∈(0, len(u)],Python将其縮寫為u[-k]。這種方法非常便利。例如,任何清單w的第一個元素都可以通過w[0]來通路,而其最後的元素可以通過w[-1]來通路。圖3-2的中間部分顯示了一個長度為8的清單的兩組索引。使用上面的代碼片段,請讀者猜測一下對應于v[1]和v[-3]的對象,并使用解釋器來檢查答案。
圖3-2 長度為8的清單u的索引和切片。中間部分顯示u的内容及其兩組索引,通過這些索引可以通路其元素。上面部分顯示了一個長度為4的切片的内容(按正序)。下面部分顯示了另一個切片的内容(按逆序)
乍一看,該功能似乎隻是一個很小的增強,但當與切片和可變性的概念結合起來時,它就變得非常強大,我們接下來将介紹這些概念。是以,讀者必須清楚地了解負數索引所代表的含義。
3.5.3 清單切片
給定一個清單u,我們可以通過切片操作來構造更多的清單。切片最簡單的形式是u[start:end],結果是一個長度為end-start的清單,如圖3-2所示。如果切片操作位于指派語句的右側,則會建立一個新的清單。例如,su=u[2:6]将建立一個包含4個元素的新清單,其中su[0]初始化為u[2]。如果切片操作位于指派語句的左側,則不會生成新的清單。相反,它允許我們改變現有清單中元素塊的值。這裡包含一些C語言和Fortran語言使用者可能不大熟悉的重要新文法。
閱讀如下簡單示例,它說明了各種可能的操作;一旦了解其含義,便建議讀者進一步嘗試自己的實驗,最好在IPython終端模式下進行操作。
1 u=[0,1,2,3,4,5,6,7]
2 su=u[2:4]
3 su
4 su[0]=17
5 su
6 u
7 u[2:4]=[3.14,'a']
8 u
如果start為0,則可以省略,例如,u[:-1]是u的除最後一個元素之外的副本。在另一端同樣适用,u[1:]是u的除第一個元素之外的副本,而u[:]是u的副本。這裡,我們假設切片操作位于指派語句的右側。切片操作的更一般的文法形式為su = u[start: end:step],結果su包含元素u[start]、u[start+step]、u[start+2*step]等,直到索引大于或者等于start+end。是以,以上述示例中的清單u為例,結果為u[2: -1:2]=[2,4,6]。一個特别有用的選項是step=-1,它允許以相反方向周遊清單。請參見圖3-2中的示例。
3.5.4 清單的可變性
對于任何容器對象u,如果可以修改其元素或者切片操作,而無須對對象辨別符進行任何明顯的更改,則可稱這樣的對象為可變對象(mutable)。特别是,清單是可變對象。對于粗心大意的程式員,可變對象将是一個陷阱。請閱讀如下代碼:
1 a=4
2 b=a
3 b='foo'
4 a
5 b
6 u=[0,1,4,9,16]
7 v=u
8 v[2]='foo'
9 v
10 u
前5行代碼很容易了解:a指派給對象4,b也指派給同一個對象。随後b指派給對象'foo',而這不會改變a。在第6行代碼中,u被指派給一個清單對象;在第7行代碼中,v也指派給同一個清單對象。由于清單是可變對象,是以我們在第8行中改變了清單對象的第2個元素。第9行顯示了改變結果。但是u同時指向同一個對象(參見圖3-1),第10行表明結果也被改變。雖然邏輯清晰無誤,但這也許不是我們期望的結果,因為u沒有被顯式地修改。
請務必牢記前面關于切片的結論:一個清單的切片總是一個新的對象,即使切片的次元與原始清單相同。是以,請把上一個代碼片段中的第6~10行與下面代碼片段進行比較:
1 u=[0,1,4,9,16]
2 v=u[ : ]
3 v[2]='foo'
4 v
5 u
該代碼片段中的第2行建立了一個切片對象,它是第1行定義的對象的一個拷貝。是以,修改清單v不會影響清單u,反之亦然。
清單是非常通用的對象,并且存在許多可以生成清單對象的Python函數。我們将在本書的其餘部分讨論清單生成。
3.5.5 元組
下一個要讨論的容器是元組(tuple)。在文法上,元組與清單的唯一差别是使用()而不是[]作為分隔符,元組同樣也支援類似于清單的索引和切片操作。然而,存在一個重要的差別:我們不能修改元組元素的值,即元組是不可變對象(immutable)。乍一看,元組似乎完全是多餘的。那為什麼不使用清單呢?然而,元組的不可變性具有一個優點:當需要一個标量時,我們可以使用元組;并且在許多情況下,當沒有歧義時,我們可以省略括号(),事實上這是使用元組的最常見方式。請閱讀如下代碼片段,我們用兩種不同的方式來對元組進行指派。
(a,b,c,d)=(4,5.0,1.5+2j,'a')
a,b,c,d = 4,5.0,1.5+2j,'a'
第2行顯示了如何使用單個指派運算符來進行多個标量指派。這在我們需要交換兩個對象,或者交換兩個辨別符(如a和L1)的常見情況下非常有效。交換兩個對象的傳統方法如下:
temp=a
a=L1
L1=temp
許多程式設計語言都采用這種方式,假設temp、a和L1都指向相同的資料類型。然而,Python語言可以采用如下方式實作相同的任務:
a,L1 = L1,a
這種方式更加清晰、簡潔,并且适用于任意資料類型。
元組的另一個用途(可能是最重要的用途)是将可變數量的參數傳遞給函數的能力,這将在3.8.4節讨論。最後,我們注意到一個經常讓初學者迷惑的文法:我們有時需要一個隻有一個元素(如foo)的元組。表達式(foo)的求值結果是去掉括号僅保留元素。正确的元組構造文法是(foo,)。
3.5.6 字元串
雖然前文已經涉及字元串,但我們注意到,Python将字元串視為包含字母數字字元的不可變容器對象。在項目之間沒有逗号分隔符。字元串分隔符既可以是單引号也可以是雙引号,但不能混合使用。未使用的分隔符可以出現在字元串中,例如:
s1="It's time to go"
s2=' "Bravo!" he shouted.'
字元串同樣支援類似于清單的索引和切片操作。
有兩個與字元串相關的非常有用的轉換函數。當函數str()應用到Python對象時,結果傳回該對象的字元串表示形式。在使用過程中,函數eval充當str()的逆函數。請閱讀如下代碼片段:
L = [1,2,3,5,8,13]
ls = str(L)
ls
eval(ls) == L
字元串對于資料的輸入非常有用,而最重要的是從列印函數生成格式化輸出(參見3.8.6節和3.8.7節)。
3.5.7 字典
如前所述,清單對象是對象的有序集合,字典對象則是對象的無序集合。我們不能根據元素的位置來通路元素,而必須配置設定一個關鍵字(一個不可變對象,通常是一個字元串)來辨別元素。是以字典是一對對象的集合,其中第一個項目是第二個項目的鍵。鍵-對象(即鍵-值)一般書寫為key:object。我們通過鍵而不是位置來擷取字典的項目。字典的分隔符是花括号{}。下面是一個簡單的例子,用于說明有關字典的基本操作。
1 empty={}
2 parms={'alpha':1.3,'beta':2.74}
3 #parms=dict(alpha=1.3,beta=2.74)
4 parms['gamma']=0.999
5 parms
被注釋了的第3行是等同于第2行的字典構造方法。第4行顯示了如何(非正式地)向字典中添加新項。這說明了字典的主要數值用法:傳遞數目不确定且可變的參數。另一個重要用途是函數中的關鍵字參數(參見3.8.5節)。一種更為複雜的應用将在8.5.2節讨論。
字典的用途非常廣泛,具有許多其他屬性,并且在更一般的上下文中有許多應用,詳情請參閱其他教科書。
3.6 Python的if語句
通常,Python按語句編寫的順序依次執行。if語句是改變執行順序的最簡單方法,每種程式設計語言都包含if語句。Python中最簡單的if語句文法格式如下:
if <布爾表達式>:
<代碼塊1>
<代碼塊2>
<布爾表達式>的求值結果必須為True或者False。如果為True,則執行<代碼塊1>,然後執行<代碼塊2>(程式的剩餘部分);如果<布爾表達式>為False,則僅執行
<代碼塊2>。注意,if語句以冒号(:)結束,這表明必須緊跟一個代碼塊。不使用括号分隔符的優點是邏輯更簡單,但缺點是必須仔細保證縮進的一緻性。所有支援Python語言的編輯器都會自動進行處理。
以下代碼片段是if語句和字元串的簡單應用示例:
x=0.47
if 0<x<1:
print "x lies between zero and one."
y=4
if語句的簡單通用文法格式如下:
<代碼塊1>
else:
<代碼塊2>
<代碼塊3>
執行<代碼塊1>或者<代碼塊2>,然後執行<代碼塊3>。
我們可以級聯if語句,并且可以使用一個簡便縮寫elif。注意,必須提供所有的邏輯代碼塊,如果特定的代碼塊不需要執行任何操作,則需要包含一條空語句pass,例如:
if <布爾表達式1>:
<代碼塊1>
elif <布爾表達式2>:
<代碼塊2>
elif <布爾表達式3>:
pass
<代碼塊4>
<代碼塊5>
如果<布爾表達式1>為True,則執行<代碼塊1>和<代碼塊5>;如果<布爾表達式1>為False并且<布爾表達式2>為True,則執行<代碼塊2>和<代碼塊5>。但是,如果<布爾表達式1>和<布爾表達式2>均為False,而<布爾表達式3>為True,則僅僅執行<代碼塊5>。如果<布爾表達式1>、<布爾表達式2>和<布爾表達式3>均為False,則執行<代碼塊4>和<代碼塊5>。
經常出現的情況是一種具有簡潔表達式的結構,例如:
if x>=0:
y=f
y=g
在C語言家族中,有一種縮寫形式。在Python語言中,上述代碼片段可以簡寫為如下清晰明了的一條語句:
y=f if x>=0 else g
3.7 循環結構
計算機能夠快速地重複一系列動作。Python包含兩種循環結構:for循環結構和while循環結構。
3.7.1 Python的for循環結構
這是最簡單的循環結構,所有的程式設計語言都包含該結構,例如C語言家族中的for循環結構和Fortran語言中的do循環結構。Python循環結構是這些循環結構中更為通用、更為複雜的演化更新。其最簡單的文法格式如下:
for <疊代變量> in <可疊代對象>:
<代碼塊>
這裡<可疊代對象>(iterable)是任何容器對象。<疊代變量>(iterator)是可以用來逐個通路容器對象的元素的任何變量。如果<可疊代對象>是一個有序容器(如清單a),那麼<疊代變量>可以是索引清單範圍内的整數i。上面的代碼将包括類似于a[i]的引用。
這些聽起來很抽象,是以需要詳細說明。許多傳統的C語言和Fortran語言的用途将被推遲到第4章,因為這些語言隻能為本章描述的核心Python提供非常低效的實作。我們從一個簡單但非正常的例子開始。
c=4
for c in "Python":
print c
此處使用c作為循環疊代變量,将覆寫前面辨別符c的用途。針對字元串疊代對象中的每一個字元,執行代碼塊(此處僅列印輸出其值)。當循環完所有的字元後,循環終止,c指向最後一個循環值。
乍一看,似乎<疊代變量>和<可疊代對象>都必須是單個對象,但我們可以通過使用元組來繞過這個要求(該方法經常被使用)。例如,假設Z是一個長度為2的元組清單,則包含兩個元組變量的文法格式如下:
for (x,y) in Z:
<代碼塊>
這是完全允許的。對于另一種更一般的用法,請參見将在4.4.1節介紹的zip函數。
在展示更傳統的用法之前,我們需要介紹Python内置的range函數。其一般文法格式如下:
range(start,end,step)
range函數生成一個整數清單:[start,start+step,start+2*step,...],每個整數都小于end。(我們在3.5.3節曾讨論過這個概念。)此處,step是可選參數,其預設值為1;start也是可選參數,其預設值為0。是以range(4)的結果為[0,1,2,3]。請閱讀如下代碼:
L=[1,4,9,16,25,36]
for it in range(len(L)):
L[it]+=1
L
注意,循環是在for語句的執行過程中設定的。代碼塊可以改變疊代變量,但不能改變循環。請嘗試運作如下簡單示例:
for it in range(4):
it*=2
print it
it
應該強調的是,這些例子中的循環體沒有太大意義,但實際不一定如此。Python提供了兩種不同的方法來動态地改變循環執行過程中的控制流。在實際情況中,它們當然可以出現在同一個循環内。
3.7.2 Python的continue語句
請閱讀如下文法格式示例:
<代碼塊1>
if <測試1>:
continue
<代碼塊2>
這裡的<測試1>傳回一個布爾值,可以假設是<代碼塊1>中行為的結果。在每次循環時,都會檢查其值,當其結果為True時,控制将傳遞到循環的頂部,進而遞增<疊代變量>,然後進行下一次循環。如果<測試1>傳回False,則執行<代碼塊2>,之後控制傳遞到循環的頂部。在循環結束(以通常方式)後,執行<代碼塊5>。
3.7.3 Python的break語句
break語句允許中斷循環,也可以通過使用一個else子句,得到不同的結果。其基本文法格式如下:
<代碼塊1>
if <測試2>:
break
<代碼塊2>
<代碼塊4>
如果在任何一次循環中<測試2>的求值結果為True,則退出循環,并且控制傳遞到<代碼塊5>。如果<測試2>的求值結果始終是False,則循環以正常的方式終止,并且控制首先傳遞到<代碼塊4>,最後傳遞到<代碼塊5>。作者發現這裡的控制流有悖直覺,但在此上下文中使用else子句是可選的,而且很少見。請閱讀以下的簡單示例。
y=107
for x in range(2,y):
if y%x == 0:
print y, " has a factor ", x
break
print y, " is prime."
3.7.4 清單解析
一種意想不到但常常需要完成的任務是:給定一個清單L1,需要構造第二個清單L2,它的元素是第一個清單相應元素的某個固定函數的值。傳統的方法是通過一個for循環來實作。例如,生成一個清單的各元素的平方的清單。
L1=[2,3,5,7,11,14]
L2=[] # Empty list
for i in range(len(L1)):
L2.append(L1[i]**2)
L2
然而,Python可以通過清單解析(list comprehension)來使用一行代碼實作這種循環操作:
L1=[2,3,5,7,11,14]
L2=[x**2 for x in L1]
L2
清單解析不僅更簡潔,而且更快速,特别是針對長清單,因為無須顯式構造for循環結構。
清單解析的用途比上述代碼更加廣泛。假設我們僅僅需要為清單L1中的奇數元素構造清單L2,則代碼如下:
L2=[x*x for x in L1 if x%2]
假設有一個平面上的點的清單,其中點的坐标存儲為元組,并且還要求計算這些點和原點的歐幾裡得距離。相應的代碼如下:
import math
lpoints=[(1,0),(1,1),(4,3),(5,12)]
ldists=[math.sqrt(x*x+y*y) for (x,y) in lpoints]
接下來有一個矩形網格的坐标點,其中x坐标存儲在一個清單中,y坐标存儲在另一個清單中。可以使用如下代碼來計算距離清單:
l_x=[0,2,3,4]
l_y=[1,2]
l_dist=[math.sqrt(x*x+y*y) for x in l_x for y in l_y]
清單解析是Python的一個特性,盡管最初不容易了解,但還是非常值得掌握的。
3.7.5 Python的while循環
Python語言支援的另一種非常有用的循環結構是while循環。其最簡單的文法格式如下:
while <測試表達式>:
<代碼塊1>
這裡<測試表達式>是一個表達式,其求值結果為布爾對象。如果求值結果為True,則執行<代碼塊1>;否則控制權轉移到<代碼塊2>。每次結束執行<代碼塊1>後,重新求<測試表達式>,并重複該過程。是以下列代碼片段在沒有外界幹擾的情況下将無限循環:
while True :
print "Type Control-C to stop this!"
和for循環結構一樣,while循環中也可以使用else、continue和break子句。continue和break子句同樣可用于縮減循環執行步驟或者退出循環。特别值得注意的是,如果上述代碼片段中使用了break子句,則将變得大有用途。這些在3.7.3節曾讨論過。
3.8 函數
函數(或者子程式)是将一系列語句組合在一起,并且可以在程式中執行任意次數。為了增加通用性,我們提供可以在調用時改變的輸入參數。函數可以傳回資料,也可以不傳回資料。
在Python中,函數和其他任何東西一樣,都是對象。我們首先讨論函數的基本文法和作用範圍的概念,然後在3.8.2~3.8.5節中讨論輸入參數的性質。(這個順序似乎不合邏輯,但輸入參數的多樣性是極其豐富的。)
3.8.1 文法和作用範圍
Python函數可以定義在程式的任何地方,但必須是在實際使用之前。其基本文法如下面的僞代碼所示:
def <函數名稱>(<形參清單>):
<函數體>
關鍵字def表示函數定義的開始;<函數名稱>指定一個辨別符或者名稱來命名函數對象。可以使用滿足通正常則的辨別符名稱,當然稍後也可以修改辨別符名稱。括号()是必需的。在括号中,可以插入用逗号分隔的零個、一個或者多個變量名,稱之為參數。最後的冒号也是必需的。
接下來是函數的主體,即要執行的語句系列。正如我們已經看到的,這些代碼塊必須縮進。函數體的結束由傳回到與def語句相同的縮進水準來辨別。在極少數情況下,我們可能需要定義一個函數,但延遲實作函數體的内容;在這個初始階段,函數體應該使用一條空語句pass。雖然不是必需的,但這是慣例并且強烈推薦,在函數頭和函數體之間包含文檔字元串(docstring),用以描述函數的具體功能。文檔字元串是用一對三引号括起來的任意格式的文本,可以跨越一行或者多行。包含文檔字元串資訊可能看起來無關緊要,但事實上十分重要。函數len的作者編寫了該函數的文檔字元串,是以使用者可以在3.5.1節通過len?擷取文檔字元串的幫助資訊。
函數體的定義引入了一個新的私有名稱空間,當函數體代碼的執行結束時,該私有名稱空間将被銷毀。調用函數時,這個名稱空間将導入def語句中作為參數的辨別符,并将指向調用函數時參數所指向的對象。在函數體中引入的新辨別符也屬于這個名稱空間。當然,該函數是在包含其他外部辨別符的名稱空間内定義的。那些與函數參數或者函數體中已經定義好的辨別符具有相同名稱的辨別符在私有名稱空間中不存在,因為它們會被那些私有參數覆寫。其他名稱在私有名稱空間中是可見的,但強烈建議不要使用它們,除非使用者絕對确定每次調用函數時它們都将指向相同的對象。為了在定義函數時確定可移植性,嘗試隻使用參數清單中包含的辨別符以及在私有名稱空間中定義的辨別符,這些名稱隻屬于私有名稱空間。
通常我們要求函數生成一些對象或者相關變量(例如y),這可以通過傳回語句來實作,例如return y。函數在執行傳回語句之後會退出,即傳回語句将是最後執行的語句,是以通常是函數體中的最後一條語句。原則上,y應該是标量,但這很容易通過使用元組來規避。例如,為了傳回三個标量(例如u、v和w),應該使用元組,例如return (u, v, w),甚至直接使用return u, v, w。如果沒有傳回語句,則Python會插入一條不可見的傳回語句return None。這裡None是一個特殊的Python變量,它指向一個空對象,并且是函數傳回的“值”。這樣,Python就避免了Fortran語言中必須将函數和過程分開的二分法。
下面是一些簡單的用于說明上述特性的示例。請嘗試在解釋器中輸入并運作這些代碼片段,以便驗證上面讨論的知識點,并進行進一步的實驗。
1 def add_one(x):
2 """ Takes x and returns x+1. """
3 x = x+1
4 return x
5
6 x=23
7 add_one? # or help(add_one)
8 add_one(0.456)
9 x
在第1~4行中,我們定義函數add_one(x)。在第1行中隻有一個參數x,它引用的對象在第3行中被改變。在第6行中,我們引入了一個由x引用的整數對象。接下來的兩行代碼分别對文檔字元串和函數進行測試。最後一行檢查x的值,該值保持不變且一直為23,盡管我們在第8行中隐式地将x指派為一個浮點數。
接下來請閱讀如下包含錯誤的代碼:
1 def add_y(x):
2 """ Adds y to x and returns x+y. """
3 return x+y
4
5 add_y(0.456)
在第5行,私有變量x被指派為0.456,而第3行查找私有名稱y;但沒有找到,是以函數在包含該函數的封閉名稱空間中查找辨別符y,結果也找不到,故而Python終止運作,并列印輸出一個錯誤。但是,如果我們在調用函數之前引入y的執行個體:
y=1
add_y(0.456)
雖然函數按預期正常運作,但這是不可移植的行為。隻有在y的執行個體已經被定義的情況下,我們才能使用名稱空間内的函數。在一些情況下,該條件可以得到滿足,但一般來說,應該避免使用這種類型的功能。
下面的示例顯示了更好的代碼實作,該示例還顯示了如何通過元組傳回多個值,并且顯示了函數也是對象。
1 def add_x_and_y(x, y):
2 """ Add x and y and return them and their sum. """
3 z=x+y
4 return x,y,z
5
6 a, b, c = add_x_and_y(1,0.456)
7 a, b, c
辨別符z是函數的私有名稱,并且在函數退出後不再可用。因為我們将c配置設定給z所指向的對象,是以當辨別符z消失後,對象本身不會丢失。在接下來的兩個代碼片段中,我們将展示函數是對象,并且我們可以給它們配置設定新的辨別符。(重新檢視圖3-1,可以幫助了解該知識點。)
f = add_x_and_y
f(0.456, 1)
f
在上述這些示例中,作為參數的對象都是不可變對象,是以函數調用沒有修改它們的值。然而,當參數是可變(容器)對象的時候,情況則并非如此。請參見如下示例:
1 L = [0,1,2]
2 print id(L)
3 def add_with_side_effects(M):
4 """ Increment first element of list. """
5 M[0]+=1
6
7 add_with_side_effects(L)
8 print L
9 id(L)
清單L本身并沒有變化。但是,在沒有使用指派運算符(在函數體之外)的情況下,清單L的内容可以并且已經被修改了。這種副作用(side effect)在此上下文中沒有問題,但在實際應用代碼中,則有可能導緻細微的難以覺察的錯誤。補救方法是拷貝一個副本,如下述代碼第5行所示。
1 L = [0,1,2]
2 id(L)
3 def add_without_side_effects(M):
4 """ Increment first element of list. """
5 MC=M[ : ]
6 MC[0]+=1
7 return MC
8
9 L = add_without_side_effects(L)
10 L
11 id(L)
在某些情況下,拷貝一個長清單會産生額外的開銷,進而影響代碼的速度,是以一般會避免拷貝長清單。然而,在使用帶副作用的函數之前,請牢記這句話:“過早優化是萬惡之源”,并謹慎使用帶副作用的函數。
3.8.2 位置參數
位置參數(positional argument)是所有程式設計語言的共同慣例。請閱讀如下示例:
def foo1(a,b,c):
<函數體>
每次調用函數foo1時,都必須精确指定三個參數。一個調用示例為y=foo1(3,2,1),
很顯然參數替換按照其位置順序進行。另一種調用該函數的方法是y=foo1(c=1,a=3,b=2),
這允許更加靈活的參數順序。指定三個以外個數的參數将導緻錯誤。
3.8.3 關鍵字參數
另一種函數定義形式指定關鍵字參數(keyword argument),例如:
def foo2(d=21.2, e=4, f='a'):
<代碼塊>
調用這類函數時,既可以指定所有的參數,也可以省略部分參數(省略的參數使用def語句中定義的預設值)。例如,調用foo2(f='b')将使用預設值d=21.2和e=4,進而滿足三個參數的要求。由于是關鍵字參數,是以其位置順序不重要。
在同一個函數定義中可以結合使用位置參數和關鍵字參數,但所有的位置參數都必須位于關鍵字參數之前。例如:
def foo3(a,b,c,d=21.2,e=4,f='a')
<代碼塊>
調用該函數時,必須指定三到六個參數,且前三個參數為位置參數。
3.8.4 可變數量的位置參數
我們常常事先并不知道需要多少個參數。例如,假設要設計一個print函數,則無法事先指定要列印輸出的項目的個數。Python使用元組來解決這個問題,print函數将在3.8.7節讨論。這裡有一個更簡單的示例,用于說明其文法、方法和用法。給定任意數量的數值,要求計算這些數值的算術平均值。
1 def average(*args):
2 """ Return mean of a non-empty tuple of numbers. """
3 print args
4 sum=0.0
5 for x in args:
6 sum+=x
7 return sum/len(args)
8
9 print average(1,2,3,4)
10 print average(1,2,3,4,5)
按照慣例(但不是強制性的)在定義中把元組取名為args,注意這裡的星号是必需的。第3行是多餘的示範代碼,其目的隻是為了說明所提供的參數真的被封裝成元組。注意,通過在第4行中把sum強制定義為實數,可以確定第7行中的除法按預期工作(即實數除法),即使分母是整數。
3.8.5 可變數量的關鍵字參數
Python可以完美處理下列形式的函數:該函數接受固定數量的位置參數;随後是任意數量的位置參數;再随後是任意數量的關鍵字參數—因為必須遵守“位置參數位于關鍵字參數之前”的順序規則。如前一節所述,附加的位置參數被打包成元組(由星号辨別)。附加的關鍵字參數則被封裝到字典中(由兩個星号辨別)。如下示例說明了這個過程:
1 def show(a, b, *args, **kwargs):
2 print a, b, args, kwargs
3
4 show(1.3,2.7,3,'a',4.2,alpha=0.99,gamma=5.67)
初學者不太可能主動地使用所有這些類型的參數。然而,讀者偶爾會在庫函數的文檔字元串中看到它們,是以了解其用法可以幫助了解文檔。
3.8.6 Python的輸入/輸出函數
每種程式設計語言都需要具有接受輸入資料或者輸出其他資料的函數,Python也不例外。輸入資料通常來自鍵盤或者檔案,而輸出資料通常被“列印”輸出到螢幕或者檔案。檔案輸入/輸出既可以是可讀的文本資料,也可以是二進制資料。
我們将先讨論資料輸出然後再讨論資料輸入。對于科技工作者而言,絕大多數檔案輸入/輸出将主要涉及數值資料,是以被推遲到4.4.1~4.4.3節讨論。資料的輸出是一個複雜的問題,将在本節和接下來的###3.8.7節中讨論。
從鍵盤輸入少量資料有多種方法,這裡選擇最簡單的解決方案。讓我們從如下簡單的代碼片段開始:
name = raw_input("What is your name? ")
print "Your name is " + name
執行第一條語句時會提示一個問題“What is your name?”,随後鍵盤輸入将捕獲到一個字元串。是以第二條語句的含義是輸出兩個拼接的字元串。
現在假設我們希望從鍵盤輸入一個清單(例如[1,2,3])。如果使用上面的代碼片段,則name指向一個字元串,因而需要使用語句eval(name)從字元串中構造出清單對象(參見3.5.6節)。請閱讀如下的另一個代碼片段:
ilist = input('Enter an explicit list')
ilist
假如我們輸入[1,2,3],則同樣起作用。如果預先定義了一個Python對象的辨別符objname,則也可以輸入該辨別符。類似的代碼片段應該可以處理絕大多數鍵盤輸入任務。
3.8.7 Python的print函數
到前兩節内容為止,我們并不需要輸出指令或者輸出函數。在解釋器中,我們可以通過鍵入辨別符來“輸出”任何Python對象。然而一旦開始編寫函數或者程式,我們就需要一個輸出函數。這裡有些麻煩:在Python 3.0以前的版本中,print是一條指令,其調用方法如下:
print <要列印輸出的内容>
而在Python 3.0及以後的版本中,print被實作為一個函數,于是上述代碼行書寫為:
Print(<要列印輸出的内容>)
在編寫本書時,部分NumPy及其主要擴充僅支援較早的版本。由于早期版本的Python可能最終會被廢棄,是以數值處理的使用者會面臨潛在的軟體過時的困境,然而,這裡有兩種簡單的解決方案。print函數要求一個可變數量的參數,即一個元組。如果我們在早期的Python版本中使用上面第二種代碼片段,則print指令把參數看作是一個顯式的元組,因為<要列印輸出的内容>被包含在括号中。如果感覺多餘的括号有些别扭,則可以使用另一種解決方案,即在代碼的頂部包含如下代碼行:
from future import print_function
如果在Python 3.0以前版本中使用Python 3.0及以後版本的print函數,則請删除該語句。
print指令要求使用一個元組作為參數。是以,假設it指向一個整數,y指向一個浮點數,不使用元組分隔符(括号),則可以編寫如下語句:
print "After iteration ",it,", the solution was ",y
這裡沒有控制兩個數值的格式,是以會導緻潛在的問題,例如,在輸出表格内容時需要高度一緻的格式。首先,嘗試如下稍微複雜的一種解決方法:
print "After iteration %d, the solution was %f" % (it,y)
格式化字元串的通用文法格式如下:
%
其中,格式化字元串中的%d項被替換為一個整數,而%f項則被替換為一個浮點數,這兩個數按順序從參數中的最終元組獲得。這個版本的輸出結果與上一個版本的輸出結果相同,但我們還可以進一步改進。下面列舉的格式化代碼是基于C語言家族中有關printf函數的定義。
首先考慮整數it的格式,其中it的值為41。如果把代碼中的格式化字元串%d替換為%5d,則輸出将包括5個字元,數值右對齊,也即“41”,其中字元 表示空白符。同樣%-5d将輸出左對齊結果“41”。另外,%05d将輸出結果“00041”。如果數值是負數,則符号将包含在字段的計數中。是以,如果同時輸出正整數和負整數,則可能導緻結果不整齊。我們可以強制輸出以正号或者負号開始,右對齊時選擇使用%+5d,而左對齊時選擇使用%+-5d。當然,字段寬度5沒有特殊含義,可以用任何其他合适的數字來替代。實際上,當整數的精确表示要求比指定寬度更多的位數時,Python會忽略格式化字元串的訓示以保證輸出精度。
格式化輸出浮點數則有三種可能性。假設y的值為123.456789。如果把格式化字元串%f替換為%.3f,則輸出結果為123.457,即浮點數值被四舍五入到保留3位小數。格式化字元串代碼%10.3f輸出右對齊10個字元寬度的字元串,也即“123.457”,而
%-10.3f的輸出結果相同,隻是左對齊而已。與整數格式化一樣,緊跟在百分号後面的正号強制輸出結果帶正号或者負号。另外,%010.3f會把前面的空白符替換為0。
很顯然,當y非常大或者非常小的時候,%f格式将損失精度,例如z=1234567.89的情形。在輸出的時候,我們可以這樣書寫:“z=1.23456789×106”,而Python的輸出則是z=1.23456789e6。現在當輸出格式化字元串代碼為%13.4e時,應用到z,其輸出結果為“1.2346e+06”。小數點後恰好有4位數字,輸出數值為13個字元寬度的字段。在該輸出表示中,僅需要10個字元并且數值右對齊,是以左側包含3個空白符。同上,
%-13.4e的輸出結果為左對齊,%+13.4e輸出結果的前面包含一個正号和2個空白符。(%+-13.4e結果相同但是左對齊。)如果寬度少于最低10個的要求,則Python會把寬度增加到10。最後,在格式化字元串代碼中把'e'替換為'E',則結果使用大寫的字母E,例如%+-13.4E的輸出結果為“+1.2346E+06”。
有時候我們要求輸出一個絕對值範圍變化巨大的浮點數,但是希望顯示特定位數的有效數字。以上面的z為例,%.4g将輸出1.235e+06,即正好保留4位有效數字。注意,%g預設等同于%.6g。Python将選擇使用'e'和'f'中較短的一種。同樣,%.4G将在'E'和'f'之間選擇。
考慮完整性,我們注意到,Python也提供字元串變量的格式化字元串代碼,例如,%20s将輸出一個至少有20個字元寬度的字元串,如果需要則在左邊填充空白符。
3.8.8 匿名函數
很顯然可以任意指定一個函數參數的名稱,即f(x)和f(y)指向同一個函數。另一方面,我們在函數add_x_and_y(見3.8.1節)的代碼片段中觀察到,可以改變f的名稱而不會導緻不一緻。這是數學邏輯的基本原理,通常用lambda演算(也即λ演算)的形式論來描述。在Python編碼中,會出現函數名稱完全不相關的情況,并且Python可以模拟lambda演算。我們可以把add_x_and_y編寫為如下代碼:
f= lambda x, y : x, y, x+y
或者:
lambda x,y:x,y,x+y
有關匿名函數的實際應用案例,請參見4.1.5節和8.5.3節。
3.9 Python類簡介
在Python語言中,類是極其通用的結構。正因如此,有關類的文檔既冗長又複雜。參考書籍中的例子通常不是取材于科學計算中的資料處理,而且往往過于簡單或過于複雜。基本思想是,讀者可能擁有經常發生的固定資料結構或者對象,以及直接與之關聯的操作。Python類既封裝對象又封裝其操作。我們用一個科學計算示例來進行簡單的介紹性陳述,但是卻包含許多科學家最常用的特性。在這一教學背景下,我們将使用整數運算。
我們以分數為例。分數可以被認為是實數的任意精度表示。我們考慮将一個Frac類實作為一對整數num和den,其中den為非0整數。值得注意的是,3/7和24/56通常被視為相同的數值。我們已經在第2章的後半部分讨論了這個特殊的問題,需要使用第2章建立的檔案gcd.py。下面的代碼片段顯示了Frac類的基本結構(借助于gcd.py檔案)。
1 # File frac.py
2
3 import gcd
4
5 class Frac:
6 """ Fractional class. A Frac is a pair of integers num, den
7 (with den!=0) whose GCD is 1.
8 """
9
10 def __init__(self,n,d):
11 """ Construct a Frac from integers n and d.
12 Needs error message if d=0!
13 """
14 hcf=gcd.gcd(n, d)
15 self.num, self.den = n/hcf, d/hcf
16
17 def __str__(self):
18 """ Generate a string representation of a Frac. """
19 return "%d/%d" % (self.num,self.den)
20
21 def __mul__(self,another):
22 """ Multiply two Fracs to produce a Frac. """
23 return Frac(self.num*another.num, self.den*another.den)
24
25 def __add__(self,another):
26 """ Add two Fracs to produce a Frac. """
27 return Frac(self.num*another.den+self.den*another.num,
28 self.den*another.den)
29
30 def to_real(self):
31 """ Return floating point value of Frac. """
32 return float(self.num)/float(self.den)
33
34 if __name__=="__main__":
35 a=Frac(3,7)
36 b=Frac(24,56)
37 print "a.num= ",a.num, ", b.den= ",b.den
38 print a
39 print b
40 print "floating point value of a is ", a.to_real()
41 print "product= ",a*b,", sum= ",a+b
這裡的第一個新知識點是第5行中的class語句。請注意終止冒号。實際的類是通過縮進代碼塊定義的,即示例中從第5行一直到第32行。第6~8行定義了類的文檔字元串,用于線上幫助文檔。在類體中,我們定義了五個類函數,函數相對于類縮進(因為它們屬于類成員),并且函數也有進一步縮進的函數體(和通常一樣)。
第一個類函數從第10~15行,這在其他程式設計語言中被稱為“構造函數”,其目的是把一對整數轉換為一個Frac對象。函數的名稱必須為__init__(似乎有點奇怪),但我們将看到,在類的外部從來不會用到該名稱。類函數的第一個參數通常被稱為self,同樣隻出現在類的定義中。這些都顯得十分陌生,是以接下來我們看看位于第35行的解釋測試集代碼a=Frac(3,7)。這條語句把一對整數3和7對應的Frac對象指派給辨別符a。該語句隐式地調用__init__函數,并使用a代替self,3和7代替n和d。然後計算hcf,即3和7的最大公約數(GCD,結果為1),并在第15行代碼中計算a.num=n/hcf和a.den=d/hcf。這些都可以以通常的方式進行通路,是以第37行代碼列印輸出分數的分子和分母的值。同理,在第36行代碼的指派語句中,調用__init__,并使用b替換self。
幾乎所有的類都能通過提供類似第38~39行的代碼而獲得友善,即“列印輸出”類的對象。這就是類的字元串函數__str__的目的,在代碼的第17~19行中進行了定義。當執行第38行的代碼時,将調用__str__函數,用a替換self;結果第19行傳回字元串"3/7",而這就是列印輸出的結果。
雖然這些類的函數或多或少都比較簡單,但我們可以定義許多(或者一些)函數來執行類操作。讓我們先關注乘法和加法,根據分數标準的運算規則:
兩個Frac對象的乘法要求定義第21~23行的類的函數__mul__。當在第41行中調用ab時,将調用該函數,使用左側操作數(即a)替換self,使用右側操作數(即b)替換another。注意第23行代碼計算乘積的分子和分母,然後調用__init__來建立一個新的Frac對象,是以c=ab将建立一個辨別符為c的新的Frac對象。而在第41行代碼中,則建立一個匿名的Frac對象并立即傳遞給__str__。上面的代碼片段使用了同樣的方法來處理加法運算。
注意在第37行代碼中,我們可以直接通路類的對象執行個體a的構成部分。同樣,在第40行代碼中,我們使用了與類執行個體相關聯的函數to_real()。它們都是類的屬性,我們将越來越多地使用“點(.)通路機制”,這是Python最廣泛的使用方式,請參見下一節。
考慮到簡潔性,我們沒有實作類的所有功能。對于初學者而言,以更進階的方式逐漸完善代碼将是非常有益的訓練。
1.分别建立名為__div__和__sub__的除法和減法類函數,并測試。
2.如果分母為1,則列印輸出的結果會顯得有些奇怪,例如7/1。請改進__str__函數,使得當self.den為1時,建立的字元串恰好是self.num,并測試新版本是否正常工作。
3.當參數d為0時,很顯然__init__會出錯。請為使用者提供警告資訊。
3.10 Python程式結構
在3.2節的末尾,我們讨論了辨別符和對象的關系,這裡我們進一步展開這個話題。在3.9節有關Python類的教學示例中,我們注意到類Frac的一個執行個體對象,例如a=Frac(3,7),其将建立一個辨別符a,指向類Frac的一個對象。現在一個Frac對象包含資料(一對整數)以及若幹操作這些資料的函數。我們通過“點通路機制”來通路與對象執行個體關聯的資料,例如a.num。同樣,我們也可以通路相關聯的函數,例如a.to_real()。
到目前為止,這裡隻是總結前文所陳述的事實。然而,Python中充滿了各種各樣的對象,有些非常複雜,這種“點通路機制”被廣泛用于通路對象的元件。我們已經學習了很多有關Python的知識,下面給出一些示例。
我們的第一個示例是3.3.4節中讨論的複數。假設我們有c=1.5-0.4j,或者等價地c=complex(1.5,-0.4)。我們應該把複數看作是類Complex的對象,即使其為了保證效率而被内置到系統中。接下來,類似于Frac類的操作,我們可以通過c.real和c.imag來通路其資料,而c.conjugate()則建立了一個共轭複數1.5+0.4j。這些都是有關執行個體對象和屬性的進一步示例。我們說Python是面向對象的程式設計語言。而在面向函數的程式設計語言(例如Fortran77)中,則會使用C=CMPLX(1.5,-0.40)、REAL(C)、AIMAG(C)和CONJG(C)。編寫簡單程式時,無須側重于哪一種程式設計方法。
我們的下一個示例針對3.4節中讨論的子產品。和Python語言的其他特性一樣,子產品也是對象。是以import math as re将包含math子產品并為其指定一個辨別符re。我們可以通路其資料(例如re.pi),也可以通路其函數,例如re.gamma(2)。
一旦讀者掌握了“點通路機制”,那麼了解Python語言将變得更加簡單。例如,請參見3.5節中有關容器對象的讨論。所有更加複雜的包(如NumPy、Matplotlib、Mayavi、SymPy和Pandas)都是基于該基礎。正是在這個級别上,面向對象的方法在提供C或者Fortran的早期版本中不具備的統一環境方面占據了優勢。
3.11 素數:實用示例
本章最後通過一個實際問題來讨論“純”Python。網際網路使通信發生了革命性的變化,并且強調了安全傳輸資料的必要性。這種安全性在很大程度上是基于這樣一個事實,即給定一個大整數n(例如n>10100),很難确定其是否可以表示成若幹素數的乘積。我們來讨論一個更基本的問題:建構一個素數清單。
讓我們回顧素數的定義:一個整數p如果不能表示成整數q和r(均大于1)的乘積q×r,則p是素數。是以,最初的幾個素數是2、3、5、7…。确定小于或者等于給定整數n的所有素數的問題已經研究了幾千年,也許最著名的方法是埃拉托色尼篩選法(Sieve of Eratosthenes)。在表3-1中描述了n=18的情況。表标題說明了其篩選過程的工作原理,并且顯示了前3個步驟。注意,由于要删除的任何合數都≤n,其中至少一個因子。在本例中,,是以篩選過程不需要删除5、7…。讀者也許已經注意到,在表3-1中,我們包含了一個絕對多餘的行,其目的是闡明這一點。
表3-1 求素數(≤18)的埃拉托色尼篩選法。首先,我們在第1行寫下從2到18的整數;在第2行中,從最左邊開始删除所有2的倍數;然後我們處理最接近的剩餘整數,這裡是3,并在第3行中删除所有3的倍數。繼續這個過程。很顯然,剩下的數字不是整數的乘積,即它們是素數
直接實作該過程的Python函數代碼如下所示:
1 def sieve_v1(n):
2 """
3 Use Sieve of Eratosthenes to compute list of primes <= n.
4 Version 1
5 """
6 primes=range(2,n+1)
7 for p in primes:
8 if p*p>n:
9 break
10 product=2*p
11 while product<=n:
12 if product in primes:
13 primes.remove(product)
14 product+=p
15 return len(primes),primes
前5行是正常代碼。在第6行中,我們将表3-1的頂部行編碼為Python清單,稱為primes,用于篩選。接下來,我們介紹一個for循環(第7~14行的代碼),循環變量p的每一個值對應于表中的下一行。正如我們上面讨論的結果,我們不需要考慮,這将在第8行和第9行中測試。break指令将控制轉移到循環結束後的語句,即第15行(這可以通過縮進來觀察到)。接下來讨論while循環(第11~14行的代碼)。正是因為前面出現的break語句,才保證了while循環至少執行一次。然後,如果product仍然在清單中,則第13行會将其删除。在3.5.1節中,我們了解到list.append(item)把item附加到list中,這裡list.remove(item)從list中删除第一次出現的item。如果list中不包括item,則會出錯,是以第12行代碼確定其存在。(請讀者嘗試help(list)或者list?,以檢視有關清單的幫助文檔資訊。)删除了可能存在的2p,接下來在第14行中構造3p并重複該過程。一旦通過while循環的疊代删除了p的所有倍數,程式便傳回到第7行,并将p設定為清單中的下一個素數。最後,我們傳回素數清單及其長度。後者是數論中的一個重要函數,通常表示為π(n)。
建議讀者建立一個名為sieves.py的檔案,并輸入或者拷貝上述代碼片段到檔案中。然後在IPython中輸入如下指令:
from sieves import sieve_v1
sieve_v1?
sieve_v1(18)
這樣可以驗證程式是否正常工作。讀者還可以通過如下指令來檢查程式運作消耗的時間:
timeit sieve_v1(1000)
表3-2中的結果表明,雖然這個簡單直接的函數的性能對于小n來說是令人滿意的,但對于即便是中等大小的整數值而言,所耗費的時間也變得非常大,令人無法接受。考慮如何友善地提高其性能将是一個非常有用的練習實踐。
表3-2 素數個數π(n)≤n,以及使用上述代碼片段中的Python函數在作者筆記本電腦上計算運作耗費的大約時間。這裡關注的是相對時間(而不是絕對時間)
我們首先考慮算法,它包含兩個循環。不可避免地,我們必須對實際素數進行疊代,但是對于每個素數p,我們進行循環,并從循環中去除合數n×p,其中n=2,3,4,…。我們可以在這裡做兩處非常簡單的改進。注意,假設p>2,那麼任何小于p2的合數都将從篩子中移除,是以我們可以從移除合數p2開始。此外,如果n為奇數,則合數p2+n×p是偶數,是以也在第一遍的篩選中被去除。是以,針對每個素數p>2,我們可以通過去除p2,p2+2p,p2+4p,…來改進算法。雖然這并不是最好的方法(例如,63被篩過兩次),但這就足夠滿足要求。
接下來我們讨論代碼實作。代碼中包含了5個循環,這相當浪費。for循環和while循環都是顯式的。第12行中的if語句涉及周遊素數清單。這在第13行中被重複,而在找到并丢棄product後,清單中的剩餘元素都需要向下移動一個位置。請閱讀如下重構代碼(雖然其邏輯不那麼明顯):
1 def sieve_v2(n):
2 """
3 Sieve of Eratosthenes to compute list of primes <= n.
4 Version 2.
5 """
6 sieve = [True]*(n+1)
7 for i in xrange(3,n+1,2):
8 if i*i > n:
9 break
10 if sieve[i]:
11 sieve[i*i: :2*i]=[False]*((n - i*i) // (2*i) + 1)
12 answer = [2] + [i for i in xrange(3,n+1,2) if sieve[i]]
13 return len(answer), answer
第6行代碼中,把篩選器實作為一個布爾值清單,所有的元素都初始化為True。相比同樣長度的整數清單,布爾值清單更容易設定,并且占用更少的記憶體空間。外層循環是一個for循環(第7~11行的代碼)。第7行的xrange是一個新函數,其作用和range函數類似,差別是不會在記憶體中建立整個清單,而是按需生成清單的元素。對于大型清單,這種操作會更快并且占用更少的記憶體。這裡的循環覆寫了閉區間[3, n]中的所有奇數。和上一個版本一樣,一旦,則第8行和第9行會終止外層循環。接下來讨論第10行和第11行。開始時i為3,并且sieve[i]為True。我們需要設定篩選器的第i2,i2+2i,…個為False,其作用等同于在實作中丢棄這些元素。第11行的代碼使用了單行切片方法來實作該操作,而沒有使用循環結構(右邊的整數因子給出切片的次元)。在for循環結束時,清單sieve中與所有奇合數對應的索引位置的元素都會被設定為False。最後,第12行代碼建構出素數清單。我們從包含一個元素2的清單[2]開始,使用一個清單解析式來構造一個包含所有奇數并且未篩除的數值,并把這兩個清單拼接在一起,然後傳回結果。
雖然一開始了解這個版本的代碼可能需要一些時間,但程式并沒有使用新的知識(除了xrange),代碼長度縮短了25%,并且運作速度大大提高了,如表3-2中的最後一列所示。事實上,它能在幾秒鐘的時間内篩選出超過百萬的小于108的素數。如果擴充到109,則需要耗時幾分鐘,并且需要幾十GB的記憶體。很顯然,對于非常大的素數,篩選方法不是最有效的算法。
同時請注意,在第2個版本的清單中,所有項都具有相同的類型。雖然Python沒有強加這個限制,但引入一個新對象(同構類型的清單)是值得的,這自然而然就引出了下一章的主題:NumPy。