天天看點

《資料結構與算法:Python語言描述》一2.2Python的類

本節書摘來自華章出版社《資料結構與算法:python語言描述》一書中的第2章,第2.2節,作者 裘宗燕,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視

在讨論了抽象資料類型的基本思想和描述技術之後,現在考慮它們在python語言裡的實作。python語言裡沒有直接的adt定義,實作adt可以采用很多不同的技術。本節介紹最常用也是最自然的一種技術:利用class定義(類定義)實作抽象資料類型。本節假定學習者已有基本的python程式設計經驗,但不熟悉其中的class定義。熟悉這些内容的讀者可以跳過本章下面的内容。本節從例子出發介紹class定義的使用、其結構和主要設施。下一節将進一步讨論python中基于class的程式設計技術,稱為面向對象技術。

類(class)定義機制用于定義程式裡需要的類型,定義好的一個類就像一個系統内部類型,可以産生該類型的對象(也稱為該類的執行個體),執行個體對象具有這個類所描述的行為。實際上,python語言把内置類型都看作類。

在介紹python類定義的詳細情況之前,這裡先給出一個類定義的執行個體。下面代碼是一個簡化的有理數類的一部分:

下面對這段代碼做些解釋:

class是關鍵字,表示由這裡開始一個類定義。class之後是給定的類名和一個表示類頭部結束的冒号。這部分(這一行)稱為類定義的頭部,随後是類定義的體部分,形式上就是一個語句組。定義一個類,通常是為了建立該類的執行個體,稱為該類的執行個體對象,簡稱這個類的對象。例如,上面有理數類的名字是rational0,定義它就是為了在程式裡建立和使用rational0(一種有理數)對象。

類的體部分通常主要是一批函數定義,所定義的函數稱為這個類的方法。最常見的方法是操作本類的執行個體對象的方法,稱為執行個體方法。這種方法總是從本類的對象出發去調用,其參數表裡的第一個參數就表示實際使用時的調用對象,通常以self作為參數名。例如,rational0類裡定義了三個執行個體方法。下面讨論中簡單地說“方法”時,指的總是執行個體方法,其他情況在後面介紹。

在一個類裡,通常都會定義一個名為 init 的方法(其名字是在init的前後各加兩個下劃線符号),稱為初始化方法,其工作是構造本類的新對象。建立執行個體對象采用函數調用的描述形式,以類名作為函數名,這時系統将建立一個該類的新對象,并自動對這個對象執行 init 方法。例如,下面語句

就是要求建立一個值為3/5的有理數對象,并把這個新對象賦給變量r1。調用式應給出除self之外的其他實際參數。rational0類的__init__ 方法要求兩個實參,上面語句中是3和5。求值表達式時python系統先建立一個新對象,然後把這個對象作為 __init__方法的self參數去執行方法體。

在rational0類的__init__方法裡有兩個語句,要求用實參的值給self.num和self.den指派。在執行個體方法的體中,self.fname形式的寫法表示本類執行個體對象的屬性,其中fname稱為屬性名。與python變量的情況類似,程式裡不需要說明對象有哪些屬性,指派時就會建立。上面初始化方法要求給rational0對象的兩個屬性指派,建立本類對象時就會為它建立相應的屬性并賦以相應的值。

類裡的其他執行個體方法也應該以self作為第一個參數。對它們的調用需要從本類的執行個體出發,用圓點形式描述。如果寫

指派右邊的表達式調用方法plus,r1的值被稱為plus方法的調用對象,方法中self參數将限制于該對象。調用中的實參表達式rational0(7, 15)建立另一個有理數對象,它将作為plus方法的第二個實參限制到形參another。

上面類定義裡的print方法隻有一個self參數,其調用形式就應該是r1.print(),不要求其他實參。該方法以字元串形式輸出對象r1的内容。

在定義了類rational0之後,如果送給python系統下面語句:

解釋器将輸出80/75。容易看到這個結果沒有化簡,雖然正确卻不是最合适的形式。如果程式裡用這樣的有理數對象做複雜計算,計算結果的分子和分母都會變得越來越大。雖然python支援任意大的整數,得到的結果應該是正确的,但存儲大的整數需要大的空間,計算也更費時間。是以,實作有理數的計算時應該考慮化簡。

下面将考慮一個更完整合理的有理數實作,順便介紹類定義的更多情況。

如前所述,類定義的一類重要作用是支援建立抽象的資料類型。在建立這種抽象時,人們不希望暴露其實作的内部細節。例如,對于有理數類,不希望暴露這種對象内部是用兩個整數分别表示分子和分母。對更複雜的抽象,資訊隐藏的意義可能更重要。由于隐藏抽象的内部資訊在軟體領域意義重大,有些程式設計語言為此提供了專門機制。python語言沒有專門服務于這種需求的機制,隻能依靠一些程式設計約定。

首先,人們約定,在一個類的定義裡,由下劃線_開頭的屬性名(和函數名)都當作内部使用的名字,不應該在這個類之外使用。另外,python對類定義裡以兩個下劃線開頭(但不以兩個下劃線結尾)的名字做了特殊處理,使得在類定義之外不能直接用這個名字通路。這是另一種保護方式。下面定義更好的有理數類時将遵循這些約定。

上節最後說到有理數的化簡問題。在建立有理數時,應該考慮約去其分子和分母的最大公約數,避免無意義的資源浪費。為了完成化簡,需要定義一個求最大公約數的函數gcd。這裡出現了一個問題:應該在哪裡定義這個函數。稍加分析就會發現,現在出現了兩個新情況:首先,gcd的參數應該是兩個整數,它們不屬于被定義的有理數類型。此外,gcd的計算并不依賴任何有理數類的對象,是以其參數表中似乎不應該以表示有理數的self作為第一個參數。但另一方面,這個gcd是為有理數類的實作而需要使用的一種輔助功能,根據資訊局部化的原則,局部使用的功能不應該定義為全局函數。綜合這兩點情況,gcd應該是在有理數類裡定義的一個非執行個體方法。

python把在類裡定義的這種方法稱為靜态方法(與執行個體方法不同),描述時需要在函數定義的頭部行之前加修飾符 @staticmethod。靜态方法的參數表中不應該有self參數,在其他方面沒有任何限制。對于靜态方法,可以從其定義所在類的名字出發通過圓點形式調用,也可以從該類的對象出發通過圓點形式調用。本質上說,靜态方法就是在類裡面定義的普通函數,但也是該類的局部函數。

還有一個問題也需要考慮:前面簡單有理數類的初始化方法沒有檢查參數,既沒檢查參數的類型是否合适(顯然,兩個實參都應該是整數),也沒有檢查分母是否為0。此外,人們傳送給初始化方法的實參可能有正有負,内部表示應該标準化,例如,保證所有有理數内部的分母為正,用分子的正負表示有理數的正負。這些檢查和變換都應該在有理數類的初始化方法裡完成,保證構造出的有理數都是合法合規的對象。

考慮了上面這些問題後,可以給出下面的有理數類定義(部分):

在這個類裡定義了一個局部使用的求最大公約數的靜态方法 _gcd,在初始化方法裡使用。初始化函數在開始處檢查參數的類型和分母的值,不滿足要求抛出适當的異常。随後的if語句提取有理數的符号,幾個檢查之後sign值為1表示是正數,-1表示是負數。最後用化簡後的分子和分母設定有理數的資料屬性。

下面考慮rational類的其他方法。首先,在上面定義中把有理數對象的兩個屬性都當作内部屬性,不應該在類之外去引用它們。但實際計算中有時需要提取有理數的分子或分母。為滿足這種需要,應該定義一對解析操作(也是執行個體方法):

現在考慮有理數的運算。在前面的簡單有理數類裡定義了名字為plus的方法。對于有理數這種數學類型,人們可能更希望用運算符(+、-、、/ 等)描述計算過程,寫出形式更自然的計算表達式。python語言支援這種想法,它為所有算術運算符規定了特殊方法名。python中所有特殊的名字都以兩個下劃線開始,并以兩個下劃線結束。例如,與+運算符對應的名字是__add__,與 對應的名字是__mul__。下面是實作有理數運算的幾個方法定義,其他運算不難類似地實作:

這裡有幾個問題值得提出。首先,通過在每個方法最後用rational(...,...) 構造新對象,所有構造出的對象都保證能化簡為最簡形式,不需要在每個建立新有理數的地方考慮化簡問題。這種做法很值得提倡。

另外,上面定義除法時用的是整除運算符“//”。在除法方法的開始檢查除數并可能抛出異常,也是正常的做法。按照python的慣例,普通除法“/”的結果應是浮點數,對應方法名是__truediv__,如果需要可以另行定義,實作從有理數到浮點數的轉換。

還請注意一個情況:算術運算都要求另一個參數也是有理數對象。如果希望檢查這個條件,可以在方法定義的開始加一個條件語句,用内置謂詞isinstance(another, rational) 檢查。另外,由于another是另一個有理數對象,上面方法定義中沒有直接去通路其成分,而是通過解析函數。

有理數對象經常需要比較相等和不等,有些類的對象需要比較大小。python為各種關系運算提供了特殊方法名。下面是有理數相等、小于運算的方法定義:

不等、小于、大于等運算可以類似地實作。

為了便于輸出等目的,人們經常在類裡定義一個把該類的對象轉換到字元串的方法。為了保證系統的str類型轉換函數能正确使用,這個方法應該采用特殊名字 __str__,内置函數str将調用它。下面是有理數類的字元串轉換方法:

至此一個簡單的有理數類就基本完成了,其中缺少的一些運算的定義情況類似,讀者自己完成已經沒有實質性困難。

在程式定義好一個類之後,就可以像使用python系統裡的類型一樣使用它。首先是建立類的對象,形式是采用類名的函數調用式,前面已經說明:

five = rational(5) # 初始化方法的預設參數保證用整數直接建立有理數

如果一個變量的值是這個類的對象,就可以用圓點記法調用該類的執行個體方法:

由于有理數類定義了str轉換函數,可以直接用标準函數print輸出:

對于有理數,可以使用類中定義的算術運算符和條件運算符:

還可以獲得對象的類型,或者檢查對象和類的關系:

總而言之,從使用的各方面看,用類機制定義的類型與python系統的内部類型沒有什麼差别,地位和用法相同。python标準庫的一些類型就是這樣定義的。

本書後面章節将主要采用python的面向對象技術和類結構定義各種資料類型,為了更好地與之對應,這裡對adt的描述形式做一點改動。後面使用的adt描述将模仿python類定義的形式,也認為adt描述的是一個類型,是以:

adt的基本建立函數将以self為第一個參數,表示被建立的對象,其他參數表示為正确建立對象時需要提供的其他資訊。

在adt描述中的每個操作也都以self作為第一個參數,表示被操作對象。

定義二進制運算時也采用同樣的形式,其參數表将包括self和另一個同類型對象,操作傳回的是運算生成的結果對象。

雖然python函數定義的參數表裡沒有描述參數類型的機制,但為了提供更多資訊,在下面寫adt定義時,有時還是采用寫參數類型的形式,用于說明操作對具體參數的類型要求。在很多情況下,這樣寫可以省略一些文字說明。

按這種方式描述的有理數對象adt如下:

《資料結構與算法:Python語言描述》一2.2Python的類