天天看點

《資料結構與算法:Python語言描述》一2.5類定義執行個體:學校人事管理系統中的類

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

作為本章内容的總結,現在考慮一個綜合性的執行個體:為一個學校的人員管理系統定義所需的表示人員資訊的類,它們都是資料抽象。

學校裡有兩大類人員,即學生和教職工,他們都是需要在系統裡表示的對象。分析這兩類人員需要記錄的資訊,可以看到這裡有一些值得注意的情況:與兩類人員有關的資訊中存在一些公共部分,又有各自的特殊情況:

首先,作為人員資訊,無論學生或教職工都有姓名、性别、年齡等公共資訊。另外,為了便于學校管理,學生應該有一個學号,教職工也應該有一個職工号。

作為學生應該有學習記錄,包括屬于哪個院系、注冊時間,特别是學習期間已經學過的各門課程及其成績等。

教職工應該有入職時間、院系、職位和工資等資訊。

由于兩類人員的資訊既有共性又有特殊性,特别适合采用面向對象的類繼承機制處理。這裡的考慮是首先定義一個公共的人員類,提供記錄和查詢人員基本資訊的功能。然後從這個公共類分别派生出表示學生的類和表示教職工的類。

顯然,表示人員或者其特殊情況(學生或教職工)都是資料表示問題,應該采用抽象資料類型的思想對其進行分析和設計。在開始具體的類定義(程式設計)之前,下面首先考慮如何設計出幾個合适的抽象資料類型(adt)。

基本人員adt的設計

首先考慮一般人員adt的定義。為建立這個adt的具體對象,需要提供一組基本資訊,包括有關人員的姓名、性别、出生年月日和一個人員編号(學号或職工号,這裡要求提供,具體人員類adt可以考慮具體的生成規則)。adt的解析操作包括提取人員的編号、姓名、性别、出生年月日和年齡。還應允許人員改名,為此定義一個變動操作。由于人員記錄可能需要排序,為此要有一個對象之間的“小于”運算符。還需要為輸出定義一些輔助操作。根據這些考慮,可以給出下面抽象資料類型定義:

《資料結構與算法:Python語言描述》一2.5類定義執行個體:學校人事管理系統中的類

為管理一個學校的人員,還需考慮人數統計。這件事可以通過類裡的資料屬性和類方法完成,在下面的實作中可以考慮這個問題。

學生adt的設計

每個學生屬于一個院系,入學時确定。另外,學生的學号編制應該按一套規則自動生成,不需要人為選擇。新的解析操作包括檢視學生所屬院系和入學時間(年),檢視學生成績單。變動操作應包括設定選課記錄和課程成績(是變動操作)等。

根據上述分析,可以給出下面抽象資料類型定義。這裡借用了python面向對象機制中類繼承的形式,student(person) 表示繼承person抽象資料類型中除構造函數之外的其他操作。可以不引進這種縮寫,但需要把person adt的定義體重抄一遍。

《資料結構與算法:Python語言描述》一2.5類定義執行個體:學校人事管理系統中的類

實作這個adt時還需要實作一個生成學号的内部函數。具體技術後面考慮。

教職工adt的設計

與學生的情況相對應,教職工adt應有取得其院系、工資、入職時間等的解析函數,以及設定這些資料的變動操作:

《資料結構與算法:Python語言描述》一2.5類定義執行個體:學校人事管理系統中的類

完成了這些設計之後,下面可以進一步考慮其程式實作了。

這裡的考慮是定義幾個類,實作前面設計的各個adt。在定義這些類之前先定義兩個異常類,以便在定義人事類的操作中遇到異常情況時引發特殊的異常,使用這些類的程式部分可以正确地捕捉和處理。

人們在定義自己的特殊異常類時,多數時候都采用最簡單的方式:隻是簡單選擇一個合适的python标準異常類作為基類,派生時不定義任何方法或資料屬性。針對準備實作的類定義,這裡派生兩個專用的異常類:

兩個類的體都隻有一個pass語句,隻是為了填補文法上的缺位。在下面操作中遇到參數的類型或者值不滿足需要時就引發這兩個異常。

此外,由于人事類定義需要處理一些與時間有關的資料,直接采用python标準庫的有關功能類是最合适的。引進datatime标準庫包:

下面讨論幾個人事管理類的實作。

公共人員類的實作

首先考慮基本人員類的定義,将這個類命名為person。

為了統計在程式運作中建立的人員對象的個數,需要為person類引進一個資料屬性_num,每當建立這個類的對象就将其值加一。person類的__init__方法裡完成這一工作。下面是person類的開始部分:

__init__方法的主要工作是檢查參數合法性,設定對象的資料屬性。這些檢查非常重要,隻有通過細緻檢查,才能保證建立起的人員對象都是合法對象,使用這些對象的程式可以依賴于它們的合法性。對人員的名字,這裡隻要求它是一個字元串。對于性别,要求實參是兩個漢字字元串之一,用運算符in檢查。

最麻煩的問題是出生日期的檢查。樸素的考慮可能是要求實參為一個三元組,三個元素都是整數,分别表示年、月、日。但是不難想到,并非任意三個整數都構成合法的年月日資料,例如 (2015, 23, 48) 就不是。可以自己實作完整的檢查,但是很麻煩。上面的函數定義利用了标準庫包datetime裡的date類,其構造函數要求三個參數,如果實參不是合法日期值就會引發異常。在調用date構造日期對象時使用了拆分實參的描述方式。在上面方法定義裡,try語句的異常處理器沒有給定異常名,這說明它将捕捉構造date對象時發生的所有異常,處理器的體說明在這種情況下引發personvalueerror。後面幾個語句都很簡單,最後一個語句完成生成執行個體的計數工作。

person類的其他方法都非常簡單:

這裡有幾個小問題需要做一點解釋:①标準庫date類的today方法傳回函數調用時刻的日期,這也是一個date對象。date對象的year屬性記錄其年份,上面定義的age方法利用這些功能計算出兩個年份之差,得到這個人的年齡。②實作小于運算的方法要求另一個參數也是person,然後根據兩個人員記錄的_id域的大小确定記錄的大小關系。其餘情況都非常簡單,無須贅述。

在這個類裡還需要定義一個類方法,以便取得類中的人員計數值。另外定義了兩個與輸出有關的方法,它們都很簡單:

這裡的想法是讓__str__提供對象的基本資訊,details方法提供完整細節。請注意,字元串的join方法要求參數是可疊代對象,這裡先做出一個元組。

至此person類的基本定義就完成了。下面是使用這個類的幾個語句:

由于定義了__str__方法,是以可以直接用print輸出person對象。後幾個語句還展示了可以對人員對象的表排序(表的sort方法裡使用了“小于”運算符),以及通過person類名調用類方法num的情況。

總而言之,這個類實作了前面adt要求的功能。

學生類的實作

現在考慮學生類student的實作。在這裡需要關注幾件事:①student對象也是person對象,是以,在建立student對象時,應該調用person類的初始化函數,建立起表示person對象的那些資料屬性。②這裡希望student類實作一種學号生成方式。為了保證學号的唯一性,最簡單的技術就是用一個計數變量,每次生成學号時将其加一。這個變量應該是student類内部的資料,但又不屬于任何student執行個體對象,是以應該用類的資料屬性表示。③學号生成函數隻在student類的内部使用,但并不依賴于student的具體執行個體。根據這些情況,該函數似乎應該定義為靜态方法。但是,這個函數并不是獨立的,它依賴于student類中的資料屬性。根據前面的讨論,應該将其定義為類方法,在其中實作所需的學号生成規則。

基于上面考慮,student類的初始化函數定義如下:

這裡的學号用一個數字字元串表示,利用str的format方法構造學号:規定學生學号的首位為1,以便與職工号區分;把學生的入學年份用4位十進制數字的形式編碼在學号裡;最後是5位的序号。學生對象裡還要記錄學生的院系和入學報道日期,最後用一個字典記錄課程學習成績,初始時設定為空字典。

其他方法都很容易考慮,下面隻給出與選課和成績有關的三個方法:

這裡假定了必須先選課,最後才能設定課程成績。最後一個方法給出所有成績的清單,其中用了一個表描述式,非常友善。

繼續考慮可以發現一個問題:雖然person類的details方法仍然可用,但student對象包含的資訊更多,原方法不能展示這些新屬性。為了滿足student類的實際需要,必須修改details方法的行為,也就是說,需要定義一個同名的新方法,覆寫基類中已有定義的details方法。在定義這種新方法時,應該維持原方法的參數形式,并提供類似的行為,以保證派生類的對象能用在要求基類對象的環境中(“替換原理”)。此外,在新方法裡,經常需要首先完成基類同名方法所做的工作。這件事可以通過在新方法裡調用基類的同名方法實作。下面是新的details方法的定義:

當然,并不是每個派生類的覆寫方法都需要重複基類方法的工作,是否調用基類被覆寫的方法,應根據需要确定。如果需要,必須通過基類名去調用。

其餘方法都非常簡單,這裡不再給出。

教職工類的實作

下面将教職工類命名為staff,也定義為person類的子類,繼承person類的基本定義。教職工記錄對象也應包含person類對象的所有資料屬性,以便可以對它們調用person類裡定義的方法。在這些資料屬性的基礎上,staff類還需要為其執行個體擴充一些資料屬性,定義一組方法。

首先,staff類也需要為教職工對象實作一個職工号生成函數,同樣定義為類方法,基于staff類的資料屬性完成工作。這裡假定職工号的首字元為0,其中編碼了具體教職工的出生年份,在加上一個内定的序号。其餘方法的定義都比較自然,下面是staff類的重要部分,一些簡單的方法沒有給出,讀者很容易自己補全:

請注意:在這裡定義初始化方法和details方法時,都用super() 表示要求調用基類的方法,也是為了展示super函數的使用技術。在使用super() 時不需要提供self參數(請與student中的情況對比)。實際上,在做複雜的面向對象程式時,人們更提倡采用super函數描述這種調用,而不是直呼基類的名字。直接寫基類名将造成類定義代碼之間更密切的關聯,對程式的修改不利。

下面是一些使用這個類的語句:

本節通過幾個大學人事資訊類的定義,總結了利用python面向對象的繼承機制,以及在已有類的基礎上定義派生類時可能遇到的各種問題,展示了python面向對象程式設計的許多機制。在上面的類定義裡,還介紹了類的資料屬性和類方法的使用。

在面向對象程式設計領域,定義派生類主要有兩種用途:

1)定義基類對象中的一類特殊個體,它們具有與基類對象類似的行為,可以作為基類對象使用(替換原理),但通常還有一些自己的特殊功能。為滿足這種需要,從基類派生将能直接共享基類定義的操作,通過調用基類的初始化方法,建立派生類對象中與基類對象相同的部分。派生類對象繼承基類的方法屬性,可以用重新定義的方式覆寫原有方法,也可以定義新方法。在上面執行個體中,student和staff類都是公共人事類person的派生類,其對象都是特殊的person對象。

2)隻是為了重用基類已有的功能,而将一個類定義為派生類。實際中有時也有這種需要,主要是為了代碼的重用,這也是面向對象中繼承機制的一類用途。

在定義一個類時,有時需要儲存一些與整個類有關但并不特定于具體執行個體對象的資訊,或者需要一些與整個類有關的功能。這些就需要通過類的資料屬性和類方法實作。類的資料屬性通過類層面的指派語句定義,類方法需要用特殊字首 @classmethod描述。在上面的執行個體裡,多次使用了這方面的功能,如做對象計數,或為對象生成唯一編号等。這些在實際程式中都經常遇到。

基類和派生類是相對的。例如,為建立一個學校的人事系統,可能還需要從staff出發派生出更具體的教職工類,如教師類、職員類等,或從學生類出發派生出具體的大學生類、碩士生類、博士生類等。它們又需要有一些特殊的行為。本章後面的習題提出了一些這方面的問題,供讀者參考。

随着計算機科學技術和軟體領域的發展,人們逐漸認識到,資料的抽象和計算過程的抽象同樣重要。以建立資料抽象為目标的抽象資料類型的思想逐漸發展起來,對程式和軟體系統的設計以及程式設計語言的發展都産生了廣泛而深遠的影響。新的程式設計語言都為建立資料抽象設定了專門的結構或機制。所有設計優良的複雜軟體系統在其設計和實作的許多方面都會反映并實踐着抽象資料類型的思想。掌握抽象資料類型的基本思想和實踐技術,是從簡單程式設計走向複雜的實際應用開發曆程中的重要一步。

抽象資料類型的基本思想是抽象定義與資料表示和資料操作的實作分離。定義抽象資料類型,首先要描述好這一類型的對象與外界的接口,通過一組操作(函數)描述。這樣的接口定義在程式中劃出了一條明晰的分界:一邊是抽象資料類型的實作,可以采用适合具體需要的任何技術;另一邊是使用這個抽象資料類型的其他程式部分,它們隻需要相對于給定的操作接口定義,完全不必考慮有關功能是如何實作的。這種分離能很好地支援程式的子產品化組織,是分解和實作大型複雜系統的最重要基礎技術。

python語言裡專門用于支援資料抽象的機制是類(class)及其相關結構。解釋器處理完一個類定義後生成一個類對象。類對象也是一種複合對象,具有類定義裡描述的所有資料屬性和函數屬性,限制到給定的類名,就像函數定義将生成的函數對象限制于函數名一樣。類對象的最重要功能就是可以通過調用的形式生成該類的執行個體對象。如果類中定義了名字為__init__的初始化函數,生成執行個體對象時就會自動調用它;如果沒定義這個函數,生成的将是一個空對象。人們通常用初始化函數為執行個體對象建立資料屬性,設定執行個體對象的初始狀态。這樣生成的執行個體對象可以通過方法調用的形式,使用其所屬類中定義的各個執行個體函數。在類裡還可以定義靜态方法和類方法。

面向對象程式設計的另一個重要機制是繼承,用于支援基于已有的一個或幾個類定義新類。這樣定義的新類稱為派生類(或子類),被繼承的類稱為基類(或父類)。派生類可以利用基類的所有機制,可以重新定義基類中已有的方法,改變自己的執行個體對象的行為,或者通過定義新方法的方式擴充新執行個體對象的行為。在方法調用時,python采用動态限制規則,根據調用對象的類型确定應該調用的函數。面向對象的觀點把派生類看作基類的特殊情況,派生類的對象也看作基類的對象。如果在定義新類時不指明基類,python就認為基類是object。這樣,一個程式裡的類定義形成了一種層次結構,其中最高層的類是object,其他類都是object的派生類。對複雜的程式,開發者可以通過恰當的類層次結構設計,把程式中的各種資料組織好,以利于程式的開發和維護。

綜上所述,基于python的類機制,不僅可以定義出一個具體的抽象資料類型,而且可以定義出一組相關的具有層次關系的抽象資料類型。在定義新類型時,可以通過繼承的方式盡可能利用已有定義的功能,提高工作效率。良好設計的類層次結構還使開發者可以把所需操作定義在适當的抽象層次上,使操作盡可能地通用化。

python的異常處理機制是完全基于類和對象的概念構造起來的。系統定義了一組異常類,形成了一套标準的異常類層次結構。引發一個異常就是生成相應異常類的一個對象。python解釋器的異常查找機制設法找到與異常比對的處理器,比對條件就是發生的異常對象屬于處理器描述的異常類。在這裡應該注意,派生類的對象也是基類的對象。是以,捕捉基類異常的處理器也能捕捉屬于派生類的異常。如果使用者需要定義自己的異常,隻需要選擇一個系統異常類,基于它定義一個派生類。

練習

一般練習

複習下面概念:抽象資料類型,接口,實作,過程抽象和資料抽象,類型,内置類型和使用者定義類型,表示,不變類型和可變類型,不變對象和可變對象,類,類定義和類對象,類對象名字空間,類的屬性(資料屬性和函數屬性),類的執行個體(執行個體對象,對象),方法,執行個體方法,self參數,方法和函數,函數isinstance,初始化方法,執行個體的屬性和屬性指派,靜态方法,類方法,執行個體變量和私有變量,python的特殊方法名,繼承,基類,派生類,方法覆寫,替換原理,類層次結構,類object,函數issubclass,類方法查找,靜态限制和動态限制,函數super,python标準異常,異常類層次結構,exception異常,python異常的傳播和捕捉。

請列舉出資料類型的三類操作,說明它們的意義和作用。

3.為什麼需要初始化函數?其重要意義和作用是什麼?

設法說明在實際中某些類型應該定義為不變類型,另一些類型應該定義為可變類型。請各舉出兩個例子。

請簡要說明在定義一個資料類型時應該考慮哪些問題?

請檢查本章給出的date抽象資料類型,讨論其中操作的語義說明裡有哪些不精确之處,設法做些修改,消除描述中的歧義性。

請解釋并比較類定義中的三類方法:執行個體方法、靜态方法和類方法。

列出python程式設計中有關類屬性命名的約定。

請通過執行個體比較類作用域與函數作用域的差異。

試比較本章采用元組實作有理數和采用類實作有理數的技術,讨論這兩種不同方式各自的優點和缺點。

程式設計練習

定義一個表示時間的類time,它提供下面操作:

a)time(hours、minutes、seconds)建立一個時間對象;

b)t.hours()、t.minutes()、t.seconds()分别傳回時間對象t的小時、分鐘和秒值;

c)為time對象定義加法和減法操作(用運算符+和-);

d)定義時間對象的等于和小于關系運算(用運算符==和<)。

注意:time類的對象可以采用不同的内部表示方式。例如,可以給每個對象定義三個資料屬性hours、minutes和seconds,基于這種表示實作操作。也可以用一個屬性seconds,構造對象時算出參數相對于基準時間0點0分0秒的秒值,同樣可以實作所有操作。請從各方面權衡利弊,選擇合适的設計。

上面情況表現出“抽象資料類型”的抽象性,其内部實作與使用良好隔離,換一種實作方式(或改變一些操作的實作技術)可以不影響使用它的代碼。

請定義一個類,實作本章描述的date抽象資料類型。

擴充本章給出的有理數類,加入一些功能:

a)其他運算符的定義;

b)各種比較和判斷運算符的定義;

c)轉換到整數(取整)和浮點數的方法;

d)給初始化函數加入從浮點數構造有理數的功能(python标準庫浮點數類型的as_integer_ratio()函數可以用在這裡)。

對應運算符的特殊函數名請檢視語言手冊3.3.7節(emulating numeric types)。

本章2.2.2節中有理數類的實作有一個缺點:每次調__init__都會對兩個參數做一遍徹底檢查。但是,在有理數運算函數中構造結果時,其中一些檢查并不必要,浪費了時間。請查閱python手冊中與類有關的機制,特别是名字為__new_的特殊方法等,修改有關設計,使得到的實作能完成工作但又能避免不必要的檢查。

請基于2.5節的工作繼續擴充,為該學校人事系統定義研究所學生類、教師類和職員類。