之前為了編寫一個svm分詞的程式而簡單學了下Python,覺得Python很好用,想深入并系統學習一下,了解一些機制,是以開始閱讀《Python學習手冊(第三版)》。如果隻是想快速入門,我在推薦了幾篇文章,有其他語言程式設計經驗的人簡單看一看就可以很快地開始編寫Python程式了。
黑體表示章節, 下劃線表示可以直接在原文對應位置查到的專有技術名詞。
原書配套答案請到下載下傳,簡單注冊即可。
第三章 如何運作程式
import進行子產品導入隻能運作一次,多次運作需使用reload。
子產品往往是變量名的封裝,被認為是命名空間。例如:
替代方案是from,下面有同樣的效果:
第四章 介紹Python對象類型
雖然字元串支援多種操作,但是它具有不可變性,即原字元串不能改變,隻能用新字元串作為結果賦予一個變量。下面是一個試圖改變原字元串的操作及報錯資訊:
第五章 數字
str和repr顯示格式
浮點數運算在精确方面有缺陷。這和硬體有關,列印結果也不能完全解決。
使用小數對象可以進行修正
第六章 動态類型簡介
a = 3這個語句實際上執行了三個步驟:建立一個對象代表值3;如果還未建立,建立一個變量a;将變量與新的對象3連接配接。這時,變量a成為了對象3的一個引用,也可以看做是指針。
類型屬于對象,而不是變量,這就很好了解為什麼Python中同一個變量名可以作為不同類型的對象的引用了。
在這種機制下,每個對象都有一個引用計數器,當計數器為0時就被系統回收,這便是Python中對象的垃圾收集的方法了。
不同變量引用同一個數字或字元串時,對變量操作(eg.a=3 a=a+2)隻是建立了一個新的對象并使它引用新對象,這也是上一章提到的字元串不能改動的原因。而對于一些類型來說,有的操作确實能改變對象,如下所示:
為了讓兩個變量使用不同的對象,可以拷貝對象,使用L2=L1[:]來代替L2=L1即可。對于字典則使用D.copy()方法。标準庫的copy子產品提供了一個對任意對象的調用方法,下面兩種方式的差別暫不讨論:
這裡就出現了個問題,兩個引用是否是同一對象?可以用下面的方式判斷:
負值索引相當于從末尾倒數。-1就是最後一個元素的索引。對于s="spam",-5是個非法的索引值。分号:前的空值表示從第一個開始,後的空值表示直到最後一個。
第七章 字元串
單雙引号是一樣的,這樣允許使用者不使用轉移字元來實作帶有單或雙引号的字元串。個人認為避免了按shift才能使用雙引号“的麻煩。
此外,合并相鄰的字元串常量,如‘knight"s ‘ "knight‘s"(中間有空格)會顯示為‘knight"s knight\‘s‘。可見,最外層是單引号,為了保持原内容,Python把單引号裡的單引号改寫成了轉義字元,這個例子和書上的不同,更有助于了解。轉義字元和C很類似,多了幾種;但是Python裡沒有空字元串,Python為每個字元串儲存了内容和長度。同時,如果一個字元串中沒有合法的轉義編碼出現在"\"後,那麼它将在字元串中保留反斜線。抑制轉義的方法是在字元串前加r,如r"C:\new\text.dat",此時的\n和\t就不會被當做是轉義字元,同時,這樣做也不必把\改寫成\\。
三重引号适用于多行的字元串的直接輸入而不使用轉義字元。利用三重引号也可以實作類似C中/* */注釋掉代碼的目的。
Unicode字元串通過在前面加u獲得。
擴充分片是第三個索引,用作步進。這時完整的分片形式為X[I:J:K],其中步進為K。當步進取-1時,可以把字元串反轉,很神奇的方法。
利用分片,可以對字元串進行修改,即把新字元串加到原字元串上,再把原字元串切掉。
字元串格式化的用法與C的printf很像,不同之處在于所有參數外需要加一個(),形成%(arg1,arg2,arg3)的形式。格式化代碼請參考原書表格,通用結構:%[(name)][flags][width][.precision]code,其中name可以是字典名,這時在參數表裡提供這個字典的鍵即可。
既然字元串是對象,那麼它就有對應的方法。書上介紹了修改字元串的replace()、查找find()、把每個元素取出建立清單的list()、把清單合并成字元串的join()(可以作為list()的反操作)、提取元件的split()。
第八章 清單
用中括号表示清單,清單的組成對象是有序的,組成清單的各個對象允許不同。
用大括号表示字典,字典的組成對象是無序的,字典鍵的搜尋方式是哈希搜尋,速度很快。
可以用字典來模拟清單:使用序數作為字典的索引即可。類似地,字典可以用來表示一些稀疏矩陣。
字典的get方法用于避免不存在的鍵,如果鍵不存在,傳回值是0。
字典接口是使用方式類似字典并且實際工作都和字典一樣的一些Python擴充程式的接口。
第9章 元組、檔案及其他
用小括号表示元組,元組不能原處修改。
為了避免隻含一個元素的元組被當做表達式,使用一個逗号,寫為(40,)。逗号可以幫助識别元組,下面的也是元組的表示方式:
從檔案中讀取的是字元串,需要作為其他類型來操作時必須轉換。
eval()用來把字元串作為對象,是以也可以達到執行Python的任何表達式。
pickle子產品可以直接在檔案中存儲幾乎任何Python對象。
struct子產品提供了二進制資料的打包。打包+存入檔案,讀取檔案+解包。
在“指派VS引用”這一節,對于複合方式的指派,修改其成員會導緻所有使用該成員的對象的改變。直覺來看就是下面:
這是一個陷阱,為了避免這種情況,根據具體類型使用拷貝(比如分片、copy方法)而不是引用。
Python内部暫時存儲并重複使用短字元串,是以對同樣内容的字元串,is判定可能根據其長度為True(字元串較短時)或False(字元串較長時)。不同類型的比較(用==進行)的判定方式不一樣。
還有其他内置類型陷阱,如重複能增加層次深度,循環資料結構L=L.append(L)
第二部分練習題
2.(摘自附錄B)分片運算超出邊界(例如,L[-1000:100])可工作,因為Python會縮放超出邊界的分片(必要時,限制值可設為零和序列長度)。以翻轉的方式提取序列是行不通的(較低邊界值比較高邊界值更大,例如,L[3:1])。你會得到空分片([ ]),因為Python會縮放分片限制值,以确定較低邊界永遠比較高邊界小或相等(例如,L[3:1]會縮放成L[3:3],空的插入點是在偏移值3處)。Python分片一定是從左至右抽取,即使你用負号索引值也是這樣(會先加上序列長度轉換成正值)。注意到,Python
2.3的第三限制值分片會稍微修改此行為:L[3:1:-1]的确是從右至左抽取。
3.索引運算、分片運算以及del:對于L=[1,2,3,4], L[2] = []隻能把3變為[],而L[2:3] = []卻能删掉第三項。
4.X,Y = Y,X,左邊視為兩個對象,右邊視為一個元組,這個表達式交換了兩個引用。
第10章 Python語句簡介
絕大多數的Python程式每行一個語句,不需要分号。Python的風格就是完全不要分号,雖然在語句末加上分号也能通過。唯一需要分号的情況是一行中多個語句的分隔符。相反地,括号可以使一個語句分隔成很多行,比如用清單直覺地定義一個矩陣時。反斜線\也可以達到這個目的。
嵌套代碼隻需要保持縮進一緻即可。Python是WYSIWYG語言(what you see is what you get,所見即所得)
第11章 指派、表達式和列印
形如X+=Y的指派語句稱為增強指派語句,它有三個優點:程式員輸入減少,左側隻需要計算一次(X=X+Y中X計算兩次),優化技術會自動選擇(支援原處修改的類型可以直接原處修改)。
單一下劃線開頭的變量名不會被from module import *這樣的語句導入。前後都有雙下劃線的變量名是系統定義的變量名,對解釋器有特殊意義。雙下劃線開頭但結尾沒有雙下劃線的變量是類的本地變量(參考第19章)。
表達式語句通常用于執行可原處修改清單的清單方法,即對于清單L,L.append(3)是正确的,而L=L.append(4)是錯誤的,第二個式子右邊傳回的是None。
标準輸出的重定向方法:
為了避免忘記恢複sys.stdout,寫入log.txt也可以用:
第12章 if測試
類似于C,Python的布爾運算or是短路運算,而它傳回第一個為真的操作對象,或者是第二個為假的對象。[ ] or { } 将傳回{ }。
if選擇分支有以下幾種等價形式:
第13章 while和for循環
pass語句是無運算的占位符,為了表示文法需要語句并且還沒有任何實用的語句可寫時就可以使用它。
while和for循環都有一個可選的else語句,在循環條件不滿足時且沒有用break結束循環時使用。
C語言形式的 while((x = next()) != NULL) { ...process x...}在Python裡行不通:C語言指派語句會傳回指派後的值,而Python指派語句隻是語句,不是表達式。
Python的疊代協定:有next方法的對象會前進到下一個結果,而在末尾時引發StopIteration。所有的疊代工具内部工作都調用next,并捕捉StopIteration異常來确定何時離開。
用for修改清單時,for x in L:x+=1是無法修改的,因為它修改的是循環變量x而不是清單L。應該使用L[i] +=1的索引來控制修改。
zip()可以用于for并行修改多個對象時的情況(按最短的截斷)。它也可以用來再循環中建立清單:for (k,v) in zip(keys, vals):D[k]=v。
enumerate()用于産生偏移和元素:for (offset,item) in enumerate(S): print offset,item
基本的清單解析:L=[x+10 for x in L]
擴充的清單解析,删除檔案中的換行符:
從檔案中逐行讀取文本行的最佳方法是不要刻意去讀:
第十四章 文檔
__doc__屬性封裝了對象上的文檔,通過它可以檢視(比如函數的)注釋。
文檔字元串被認為最是用于較大、功能性的文檔,而#最好隻限于關于費解的表達式或語句的微型文檔。PyDoc系統能夠将前者取出并顯示。
第十五章 函數基礎
def是可執行代碼,直到運作了def時其定義的函數才開始存在。
由于函數的參數沒有類型規定,是以可以很友善地實作多态:
第十六章 作用域與參數
變量名解析的LEGB原則:變量名引用分為三個作用域進行查找,本地作用域(L,每次調用函數時建立)、上一級調用的本地作用域(E)、全局作用域(G,子產品作用域)、内置作用域(B,預定義的變量名如open)。僅對簡單變量生效,對于特定對象的變量如object.spam,查找規則規則完全不同。
内置作用域是一個名為__builtin__的内置子產品,import後才可以使用,這時可以用dir(__buildin__)檢視預定義的變量名。根據LEGB原則,在本地作用域定義一個新的open = ‘spam‘會導緻open()函數不能被調用。
global用于全局變量聲明。
作者認為在子產品導入時,導入其他子產品的子產品擁有了對其他子產品變量的修改權,這使得被導入子產品的維護變得複雜:維護者不知道第二個子產品什麼情況下會修改變量。是以最好的解決辦法是不這樣做,在檔案間進行通信的最好辦法就是通過調用函數,傳遞參數,然後獲得傳回值(用函數提供修改變量的接口,并且告訴維護者這個變量可以被其他子產品改變)。
工廠函數(又稱閉合),是能記住嵌套作用域的變量值的函數。示例:
對于函數參數,不可變參數是通過傳值來傳遞,可變對象(清單、字典等)是通過傳引用進行傳遞的。
多個傳回值的常用return方式是使用元組。
函數參數的四種比對方式:位置比對、關鍵字參數、所有對象、所有關鍵字/值:
相反地,調用函數在參數變量前面加*可以分解參數。
參數比對順序:調用和定義中先是非關鍵字參數(name)、然後是關鍵字參數(name=value)、*name最後是**name。
第17章 函數的進階話題
def f(x,y,z):return x+y+z和f= lambda x,y,z:x+y+z會達到同樣的效果。lambda是一個表達式,而不是語句,允許出現在def不能出現的地方。正是因為這個特點,lambda比def的使用更加靈活,比如編寫跳轉表(也即行為的清單或字典):L=[(lambda x:x**2),[(lambda x:x**3),[(lambda
x:x**4)]。出于代碼易讀性的考慮,應盡量避免嵌套的lambda。
apply的介紹略過,它可以用*和**型參數代替。(似乎在Python3.0以上版本已廢棄,待确認)
map(func,arg)可以很友善的用函數func處理清單類型的資料,而自己編寫類似的功能需要使用for來完成。
filter和reduce這兩個函數工具分别用于清單過濾和清單全元素的逐個運算。
關于清單解析,帶if條件的之前已提過,不再重複。for的應用示例:
更進一步的嵌套:
作者在這裡開了個小玩笑:“而map和filter的等效形式往往更複雜也會有深層的嵌套,這裡不進行說明,将這部分代碼留給禅師、前LISP程式員以及犯罪神經病作為練習”。
生成器函數與一般函數不同之處在于,它yield而不是return一個值,并把自己挂起,現場儲存在下一次調用。為與清單解析相區分,可以使用圓括号作為生成器表達式:
一個測試不同的疊代方法的小程式。當然,對于不同的操作,不同方法的相對速度可能不一樣,不存在所有情況下都最快的“最優方法”:
陷阱:本地變量是靜态檢測的。這意味着如果在子產品裡定義了X=99,def一個函數print X後又在函數裡X=88,那麼就會報錯。
陷阱:預設對象在def時指派,而不是調用函數時指派。
第18章 子產品:宏偉藍圖
Python進行import時搜尋目錄的順序:主目錄、PYTHONPATH環境變量目錄、标準庫目錄、.pth目錄。
第19章 子產品代碼編寫基礎
将會被用于導入的子產品檔案命名需要以.py做結尾。
當兩個不同子產品使用了相同的變量名時,不能用from,隻能用import。
(本章大部分内容都在第三章介紹過)
第20章 子產品包
import時列出路徑名稱,以點号相隔:import dir1.dir2.mod。這與平台無關,import不能使用平台特定的路徑表達方式。同時,這也表明檔案名省略了.py的原因。另外,dir1和dir2中必須包含一個__init__.py檔案(可以為空,Python首次進入其所在目錄時會執行它的内容)。每次使用路徑必須完整輸入,使用import dir1.dir2.mod as mod中定義的mod代替前面過長的路徑名可以解決這個問題。
個人認為,子產品包是為了友善同名子產品的使用不發生混淆的方式,這是軟體開發時所需要的。
第21章 進階子產品話題
_X的命名方式可以防止from *導入這個變量,然而這種方法不能阻止其他導入方式的導入,并不是一些面向對象語言中的私有聲明。
__all__會列出from *複制的變量名,與_X正相反。同樣隻對from *有效,不是私有聲明。
from __feature__ import featurename還不是很了解,好象是用選用擴充功能的方式開啟特殊的代碼編譯。
子產品可以通過檢測自己的__name__是否為"__main__"确定它是在執行還是被導入。這樣可以讓子產品在扮演兩種不同角色時發揮不同功能。
相對導入:路徑以一個點開始,定位同一個包的子產品。可以開啟__feature__中強迫導入的絕對性。很類似于Linux,兩個點表示上一級路徑。
陷阱一:頂層代碼的語句次序。被import時子產品的頂層代碼會立即執行,此時它所引用後文定義的變量将無效。
陷阱二:字元串變量是不能直接用于import語句的。可以使用exec "import" + modname來使用字元串modname。這樣做仍然有個缺點,每次執行時必須編譯import語句。更好的代替方案是string = __import__(modname),然後把string單列一行執行即可。
陷阱三:from複制變量名而不是拷貝。
陷阱四:reload不影響from導入。為了更新變量,使用.運算符來導入和修改其他子產品的變量。
陷阱五:reload、from及互動模式測試。這部分比較有啟發性,建議在原書仔細閱讀,簡要概括就還是:導入後(子產品)要重載(子產品),重載後還要重新執行import(變量)。reload和from的合作并不完美,最佳原則是使用reload和import來啟動程式。
陷阱六:重載沒有傳遞性。重載A不會重載A中import的B和C。需要這種功能時可以自己編寫一個通用工具。
陷阱七:遞歸導入。
第22章 OOP:宏偉藍圖
屬性通常是在class語句中通過指派語句添加在類中,而不是在定義類時嵌入。是以對沒有指派的對象屬性的通路會出錯。
類方法函數第一個參數通常為self(調用時不指明),但不一定叫self,位置是關鍵(來自習題5)。作為類方法直接調用時,需指明執行個體的名稱(24章)。
Python的OOP模型其實就是在對象樹中搜尋屬性。
(筆者有部分OOP基礎,是以本章具體理論和了解略去)
第23章 類代碼編寫基礎
類其實也是一種對象。
在類定義外建立的函數也可以成為方法:
第24章 類代碼編寫細節
和def一樣,class也是可執行代碼,運作時才會産生類對象。
調用超類的構造器是可以的,在子類的構造方法中使用Super.__init__()即可。
抽象超類有的方法沒有提供實作,而是由子類提供。
類的運算符重載通過修改諸如__add__(對應于+)等方法來實作。具體細節請參考原書。下面是一個修改__iter__獲得使用者定義的疊代器的例子:
__setattr__的修改可以模拟實作成員變量私有性,這裡不貼書中的源碼了。
右側方法如__radd__中,self在右側,和__add__相反。
__call__可以攔截調用,用使用函數的方法使用類。對改寫了__call__的類prod,執行個體化x = prod(2),x(3)可以直接使用。
__del__是析構器,但在Python中很少使用析構方法。
命名空間其實是普通的字典。
第25章 類的設計
無綁定類方法對象無self必須明确提供執行個體對象做第一個參數,綁定執行個體方法對象用self+函數對,不用傳遞執行個體。
委托是指把對象包裝在代理類中。
組合是一種技術,讓控制器類嵌入和引導一群對象,并自行提供接口。
(這一章主要内容是幫助讀者從面向過程向面向對象過渡,而且比較淺顯,在這方面作者推薦繼續去讀設計模式的書效果會更好,這裡就不詳細介紹了)
第26章 類的進階主題
僞私有屬性:将開頭兩個下劃線的變量名前再加上_類名。仍然不是真正的私有。
新式類從内置類型建立子類,或者直接用object作為超類。3.0以後所有類自動成為新式類。鑽石繼承在新式類裡從括号最右開始搜尋,這與經典類正相反。為了解決繼承不同類同名變量沖突,可以進行強制規定,如attr = B.attr。
__slots__用來限制類的執行個體能有的合法屬性集。
内容屬性使用攔截的方式來提供屬性,但是它本身不是成員變量。類似于改寫__getattr__,使用property()進行。
靜态方法和類方法分别需要調用staticmethod和classmethod兩個函數,前者調用不需要執行個體(執行個體調用時),後者把類傳入類方法第一個參數。
函數裝飾器在def上一行用@标明,有點像包裹函數,@A @B @C後def f()相當于f=A(B(C(f)))。
第27章 異常基礎
try/except可以用于捕捉異常并從異常中恢複,而try/final可以保證無論是否發生異常,終止行為都一定會進行。二者也可以合并使用(2.5版以後)。else在不發生異常時執行。except有幾種分句形式(請參考原書)。
rasie、assert用于觸發異常。raise後不帶參數表示重新引發目前異常(第28章)。
with/as可以用作try/final的替代方案。as後面是with後表達式的指派對象。
第28章 異常對象
字元串異常(myexc = "My exception string";raise myexc)已經在3.0以後消失,現在常用的是基于類的異常。類異常比字元串異常友善之處在于,可以在原始版本中用超類定義異常,在後續版本中使用子類來描述新的異常,這為版本維護提供了極大的友善。字元串異常的判斷方式是is而不是==(常見陷阱,29章)。
第29章 異常的設計
嵌套的try,引發異常時except會回到先前進入但未離開的try,而finally不會停止傳遞。
用try進行調試的方式,在錯誤發生時程式仍處于激活狀态,可以進行其他的測試而不是重新開始: