Python程式設計入門(8) (轉)

worldblog發表於2007-12-07
Python程式設計入門(8) (轉)[@more@]

第九章 類

是一個真正面向的語言,它只增加了很少的新語法就實現了類。它的類機制是C++ 和Modula-3的類機制的混合。Python的類並不嚴格限制對定義的修改,它依賴於使用者自覺不去修改定義。然而Python對類最重要的功能都保持了完全的威力。類繼承機制允許多個基類的繼承,匯出類可以過載基類的任何方法,方法可以基類的同名方法。物件可以包含任意多的私有資料。

用C++術語說,所有類成員(包括資料成員)是公用的,所有成員是虛擬(virtual)的。沒有特別的構建函式或銷燬函式(destructor)。如同在Modula-3中一樣,從物件的方法中要引用物件成員沒有簡捷的辦法:方法函式的必須以物件作為第一個引數,而在呼叫時則自動提供。象在Smalltalk中一樣,類本身也是物件,實際上這裡物件的含義比較寬:在Python 中所有的資料型別都是物件。象在C++或Modula-3中一樣,內建型別不能作為基類由使用者進行擴充套件。並且,象C++但不象Modula-3,多數有特殊語法的內建函式(如算術算符、下標等)可以作為類成員重定義。

9.1 關於術語

Python的物件概念比較廣泛,物件不一定非得是類的例項,因為如同C++和Modula-3而不同於Smalltalk,Python的資料型別不都是類,比如基本內建型別整數、列表等不是類,甚至較古怪的型別如也不是類。然而,Python所有的資料型別都或多或少地帶有一些類似物件的語法。

物件是有單獨身份的,同一物件可以有多個名字與其聯絡,這在其他語言中叫做別名。這樣做的好處乍一看並不明顯,而且對於非可變型別(數字、字串、序表(tuple))等沒有什麼差別。但是別名句法對於包含可變物件如列表、字典及涉及外部物件如檔案、視窗的程式有影響,這可以有利於程式編制,因為別名有些類似指標:比如,傳遞一個物件變得容易,因為這只是傳遞了一個指標;如果一個函式修改了作為引數傳遞來的物件,修改結果可以傳遞迴呼叫處。這樣就不必象Pascal那樣使用兩種引數傳遞機制。

9.2 Python作用域與名字空間

在引入類之前,我們必須講一講Python的作用域規則。類定義很好地利用了名字空間,需要了解Python如何處理作用域和名字空間才能充分理解類的使用。另外,作用域規則也是一個高階Python程式設計師必須掌握的知識。

 先給出一些定義。

名字空間是從名字到物件的對映。多數名字空間目前是用Python字典型別實現的,不過這一點一般是注意不到的,而且將來可能會改變。下面是名字空間的一些例項:Python中內建的名字(如abs()等函式,以及內建的例外名);模組中的全域性名;函式呼叫中的區域性變數名。在某種意義上一個物件的所有屬性也構成了一個名字空間。關於名字空間最重要的事要知道不同名字空間的名字沒有任何聯絡;例如,兩個不同模組可能都定義了一個叫“maximize ”的函式而不會引起混亂,因為模組的使用者必須在函式名之前加上模組名作為修飾。

另外,在Python中可以把任何一個在句點之後的名字稱為屬性,例如,在z.real中,real是一個物件z的屬性。嚴格地說,對模組中的名字的引用是屬性引用:在表示式modname.funcname 中,modname是一個模組物件,funcname是它的一個屬性。在這種情況下在模組屬性與模組定義的全域性名字之間存在一個直接的對映:它們使用相同的名字空間!

屬性可以是隻讀的也可以是可寫的。在屬性可寫的時候,可以對屬性賦值。模組屬性是可寫的:你可以寫“modname.the_answer = 42”。可寫屬性也可以用del語句閃出,如“del modname.the_answer”。

名字空間與不同時刻建立,有不同的生存週期。包含Python內建名字的名字空間當Python 解釋程式開始時被建立,而且不會被刪除。模組的全域性名字空間當模組定義被讀入時建立,一般情況下模組名字空間也一直存在到解釋程式退出。由解釋程式的最頂層呼叫的語句,不論是從一個指令碼檔案讀入的還是互動輸入的,都屬於一個叫做__main__的模組,所以也存在於自己的全域性名字空間之中。(內建名字實際上也存在於一個模組中,這個模組叫做__builtin__ )。

函式的區域性名字空間當函式被呼叫時建立,當函式返回或者產生了一個不能在函式內部處理的例外時被刪除。(實際上,說是忘記了這個名字空間更符合實際發生的情況。)當然,遞迴呼叫在每次遞迴中有自己的區域性名字空間。

一個作用域是Python程式中的一個文字區域,其中某個名字空間可以直接訪問。“直接訪問” 這裡指的是使用不加修飾的名字就直接找到名字空間中的物件。

雖然作用域是靜態定義的,在使用時作用域是動態的。在任何執行時刻,總是恰好有三個作用域在使用中(即恰好有三個名字空間是直接可訪問的):最內層的作用域,最先被搜尋,包含區域性名字;中層的作用域,其次被搜尋,包含當前模組的全域性名字;最外層的作用域最後被搜尋,包含內建名字。

一般情況下,區域性作用域引用當前函式的區域性名字,其中區域性是源程式文字意義上來看的。在函式外部,區域性作用域與全域性作用域使用相同的名字空間:模組的名字空間。類定義在區域性作用域中又增加了另一個名字空間。

一定要注意作用域是按照源程式中的文字位置確定的:模組中定義的函式的全域性作用域是模組的名字空間,不管這個函式是從哪裡呼叫或者以什麼名字呼叫的。另一方面,對名字的搜尋卻是在程式執行中動態進行的,不過,Python語言的定義也在演變,將來可能發展到靜態名字解析,在“編譯”時,所以不要依賴於動態名字解析!(實際上,區域性名字已經是靜態確定的了)。

Python的一個特別之處是賦值總是進入最內層作用域。關於刪除也是這樣:“del x”從區域性作用域對應的名字空間中刪除x的名字繫結(注意在Python中可以多個名字對應一個物件,所以刪除一個名字只是刪除了這個名字與其物件間的聯絡而不一定刪除這個物件。實際上,所有引入新名字的操作都使用區域性作用域:特別的,import語句和函式定義把模組名或函式名繫結入區域性作用域。(可以使用global語句指明某些變數是屬於全域性名字空間的)。

9.3 初識類

 類引入了一些新語法,三種新物件型別,以及一些新的語義。

9.3.1 類定義語法

 類定義的最簡單形式如下:

class 類名: . . .


如同函式定義(def語句)一樣,類定義必須先執行才能生效。(甚至可以把類定義放在if 語句的一個分支中或函式中)。在實際使用時,類定義中的語句通常是函式定義,其它語句也是允許的,有時是有用的――我們後面會再提到這一點。類內的函式定義通常具有一種特別形式的自變數表,專用於方法的呼叫約定――這一點也會在後面詳細討論。

進入類定義後,產生了一個新的名字空間,被用作區域性作用域――於是,所有對區域性變數的賦值進入這個新名字空間。特別地,函式定義把函式名與新函式繫結在這個名字空間。

當函式定義正常結束(從結尾退出)時,就生成了一個類物件。這基本上是將類定義生成的名字空間包裹而成的一個物件;我們在下一節會學到類物件的更多知識。原始的區域性作用域(在進入類定義之前起作用的那個)被恢復,類物件在這裡被繫結到了類物件定義頭部所指定的名字。

9.3.2 類物件

類物件支援兩種操作:屬性引用和例項化。屬性引用的格式和Python中其它的屬性引用格式相同,即obj.name。有效的屬性名包括生成類物件時的類名字空間中所有的名字。所以,如果象下面這樣定義類:

class MyClass: "A simple example class" i = 12345 def f(x): return 'hello world'


則MyClass.i和MyClass.f都是有效的屬性引用,分別返回一個整數和一個函式物件。也可以對類屬性賦值,所以你可以對MyClass.i賦值而改變該屬性的值。

__doc__也是一個有效的屬性,它是隻讀的,返回類的文件字串:“A simple example class”。

類例項化使用函式記號。只要把這個類物件看成是一個沒有自變數的函式,返回一個類例項。例如(假設使用上面的類):

x = MyClass()


可以生成該類的一個新例項並把例項物件賦給區域性變數x。 

9.3.3 例項物件

 我們如何使用例項物件呢?類例項只懂得屬性引用這一種操作。有兩類有效的屬性。 

第一類屬性叫做資料屬性。資料屬性相當於Smalltalk中的“例項變數”,和C++中的“資料成員”。資料成員不需要宣告,也不需要在類定義中已經存在,象區域性變數一樣,只要一賦值它就產生了。例如,如果x是上面的MyClass類的一個例項,則下面的例子將顯示值16而不會留下任何痕跡:

x.counter = 1 while x.counter < 10: x.counter = x.counter * 2 print x.counter del x.counter


類例項能理解的第二類屬性引用是方法。方法是“屬於”一個物件的函式。(在Python中,方法並不是只用於類例項的:其它物件型別也可以有方法,例如,列表物件也有append、insert 、remove、sort等方法。不過,在這裡除非特別說明我們用方法來特指類例項物件的方法)。

類物件的有效方法名依賴於它的類。按照定義,類的所有型別為函式物件屬性定義了其例項的對應方法。所以在我們的例子y,x.f是一個有效的方法引用,因為MyClass是一個函式;x.i 不是方法引用,因為MyClass.i不是。但是x.f和MyClass.f不是同一個東西――x.f是一個方法物件而不是一個函式物件。

9.3.4 方法物件

 方法一般是直接呼叫的,例如:

x.f()


在我們的例子中,這將返回字串‘hello world’。然而,也可以不直接呼叫方法:x.f 是一個方法物件,可以把它儲存起來再呼叫。例如:

xf = x.f while 1: print xf()


會不停地顯示“hello world”。 

呼叫方法時到底發生了什麼呢?你可能已經注意到x.f()呼叫沒有自變數,而函式f在呼叫時有一個自變數。那個自變數是怎麼回事?Python如果呼叫一個需要自變數的函式時忽略自變數肯定會產生例外錯誤――即使那個自變數不需要用到……

實際上,你可以猜出答案:方法與函式的區別在於物件作為方法的第一個自變數自動傳遞給方法。在我們的例子中,呼叫x.f()等價於呼叫MyClass.f(x)。一般地,用n個自變數的去呼叫方法等價於把方法所屬物件插入到第一個自變數前面以後呼叫對應函式。

如果你還不理解方法是如何工作的,看一看方法的實現可能會有所幫助。在引用非資料屬性的例項屬性時,將搜尋它的類。如果該屬性名是一個有效的函式物件,就生成一個方法物件,把例項物件(的指標)和函式物件包裝到一起:這就是方法物件。當方法物件用一個自變數表呼叫時,它再被開啟包裝,由例項物件和原自變數表組合起來形成新自變數表,用這個新自變數表呼叫函式。

9.4 一些說明

在名字相同時資料屬性會覆蓋方法屬性;為了避免偶然的名字衝突,這在大型程式中會造成難以查詢的錯誤,最好按某種命名慣例來區分方法名和資料名,例如,所有方法名用大寫字母開頭,所有資料屬性名前用一個唯一的字串開頭(或者只是一個下劃線),或方法名用動詞而資料名用名詞。

資料屬性可以被方法引用也可以被普通使用者(“客戶”)引用。換句話說,類不能用來構造抽象資料型別。實際上,Python中沒有任何辦法可以強制進行資料隱藏——這些都是基於慣例。(另一方面,Python的實現是用C寫的,它可以完全隱藏實現細節,必要時可以控制物件存取;用C寫的Python擴充套件模組也有同樣特性)。

客戶要自己小心使用資料屬性——客戶可能會因為隨意更改類物件的資料屬性而破壞由類方法維護的類資料的一致性。注意客戶只要注意避免名字衝突可以任意為例項物件增加新資料屬性而不需影響到方法的有效性——這裡,有效的命名慣例可以省去許多麻煩。

從方法內要訪問本物件的資料屬性(或其它方法)沒有一個簡寫的辦法。我認為這事實上增加了程式的可讀性:在方法定義中不會混淆區域性變數和例項變數。

習慣上,方法的第一自變數叫做self。這只不過是一個習慣用法:名字self在Python中沒有任何特殊意義。但是,因為使用者都使用此慣例,所以違背此慣例可能使其它Python程式設計師不容易讀你的程式,可以想象某些類瀏覽程式會依賴於此慣例)。

作為類屬性的任何函式物件都為該類的例項定義一個方法。函式的定義不一定必須在類定義內部:只要在類內把一個函式物件賦給一個區域性變數就可以了。例如:

# Function defined outs the class def f1(self, x, y): return min(x, x+y)   class C: f = f1 def g(self): return 'hello world' h = g


現在f、g和h都是類C的屬性且指向函式物件,所以它們都是C的例項的方法——其中h與g 完全等價。注意我們應該避免這種用法以免誤導讀者。

 方法可以用代表所屬物件的self自變數來引用本類其它的方法,如:

class Bag: def empty(self): self.data = [] def add(self, x): self.data.append(x) def addtwice(self, x): self.add(x) self.add(x)


例項化操作(“呼叫”一個類物件)生成一個空物件。許多類要求生成具有已知初識狀態的類。為此,類可以定義一個特殊的叫做__init__()的方法,如:

def __init__(self): self.empty()


一個類定義了__init__()方法以後,類例項化時就會自動為新生成的類例項呼叫呼叫__init__() 方法。所以在Bag例子中,可以用如下程式生成新的初始化的例項:

x = Bag()


當然,__init__()方法可以有自變數,這樣可以實現更大的靈活性。在這樣的情況下,類例項化時指定的自變數被傳遞給__init__()方法。例如:

>>> class Complex: ... def __init__(self, realpart, imagpart): ... self.r = realpart ... self.i = imagpart ... >>> x = Complex(3.0,-4.5) >>> x.r, x.i (3.0, -4.5)


方法可以和普通函式一樣地引用全域性名字。方法的全域性作用域是包含類定義的模組。(注意類本身並不被用作全域性作用域!)雖然我們很少需要在方法中使用全域性資料,全域性作用域還是有許多合法的用途:例如,匯入全域性作用域的函式和模組可以被方法使用,在同一模組中定義的函式和方法也可以被方法使用。包含此方法的類一般也在此全域性作用域中定義,下一節我們會看到一個方法為什麼需要引用自己的類!

9.5 繼承

當然,一個語言如果不支援繼承就談不到“類”。匯出類的定義方法如下:

class 匯出類名(基類名): . . .


其中“基類名”必須在包含匯出類定義的作用域中有定義。除了給出基類名外,還可以給出一個表示式,在基類定義於其它模組中時這是有用的,如:

class 匯出類名 (模組名.基類名):

匯出類定義的執行和基類執行的方法是一樣的。生成類物件是,基類被記憶。這用於解決屬性引用:如果類中未找到要求的屬性就到基類中去查詢。如果基類還有基類的話這個規則遞迴地應用到更高的類。

匯出類在例項化時沒有任何特殊規則。“匯出類名()”產生該類的一個新例項。方法引用這樣解決:搜尋相應類屬性,如果必要的話逐級向基類查詢,如果找到了一個函式物件就是有效的方法引用。

匯出類可以重寫基類的方法。因為方法在呼叫同一物件的其它方法時並無任何特殊,如果基類中某一方法呼叫同一基類的另一方法,在匯出類中該方法呼叫的就可能是已經被匯出類重寫後的方法了。(對C++程式設計師而言:Python中所有方法都是“虛擬函式”)。

匯出類中重寫的方法可能是需要擴充基類的同名方法而不是完全代替原來的方法。匯出類呼叫基類同名方法很簡單:“基類名.方法名(self, 自變數表)”。對類使用者這種做法偶爾也是有用的。(注意只有基類在同一全域性作用域定義或匯入時才能這樣用)。

8.5.1 多重繼承

Python也支援有限的多重繼承。有多個基類的類定義格式如下:

class 匯出類名 (基類1, 基類2, 基類3): . . .


關於多重繼承只需要解釋如何解決類屬性引用。類屬性引用是深度優先,從左向右進行的。所以,如果在匯出類定義中未找到某個屬性,就先在基類1中查詢,然後(遞迴地)在基類1 的基類中查詢,如果都沒有找到,就在基類2中查詢,如此進行下去。

(對某些人來說寬度優先——先在基類2和基類3中查詢再到基類1的基類中查詢——看起來更自然。然而,這需要你在確定基類1與基類2的屬性衝突時明確知道這個屬性是在基類1本身定義還是在其基類中定義。深度優先規則不區分基類1的一個屬性到底是直接定義的還是繼承來的)。

很顯然,如果不加地使用多重繼承會造成程式維護的惡夢,因為Python避免名字衝突只靠習慣約定。多重繼承的一個眾所周知的問題是當匯出類有兩個基類恰好從同一個基類匯出的。儘管很容易想到這種情況的後果(例項只有一份“例項變數”或資料屬性被共同的基類使用),但是這種做法有什麼用處卻是不清楚的。

9.6 私有變數

Python對私有類成員有部分支援。任何象__spam這樣形式的識別符號(至少有兩個前導下劃線,至多有一個結尾下劃線)目前被替換成_classname__spam,其中classname是所屬類名去掉前導下劃線的結果。這種攪亂不管識別符號的語法位置,所以可以用來定義類私有的例項、變數、方法,以及全域性變數,甚至於儲存對於此類是私有的其它類的例項。如果攪亂的名字超過255個字元可能會發生截斷。在類外面或類名只有下劃線時不進行攪亂。

名字攪亂的目的是給類一種定義“私有”例項變數和方法的簡單方法,不需擔心它的其它類會定義同名變數,也不怕類外的程式碼弄亂例項的變數。注意攪亂規則主要是為了避免偶然的錯誤,如果你一定想做的話仍然可以訪問或修改私有變數。這甚至是有用的,比如程式要用到私有變數,這也是為什麼這個沒有堵上的一個原因。(小錯誤:匯出類和基類取相同的名字就可以使用基類的私有變數)。

注意傳遞給exec,eval()或evalfile()的程式碼不會認為呼叫它們的類的類名是當前類,這與global語句的情況類似,global的作用侷限於一起位元組編譯的程式碼。同樣的限制也適用於getattr() ,setattr()和delattr(),以及直接訪問__dict__的時候。

下面例子中的類實現了自己的__getattr__和__setattr__方法,把所有屬性儲存在一個私有變數中,這在Python的新舊版本中都是可行的:

class VirtualAttributes: __vdict = None __vdict_name = locals().keys()[0] def __init__(self): self.__dict__[self.__vdict_name] = {} def __getattr__(self, name): return self.__vdict[name] def __setattr__(self, name, value): self.__vdict[name] = value


9.7 補充

有時我們希望有一種類似Pascal的“record”或C的“struct”的型別,可以把幾個有名的資料項組合在一起。一個空類可以很好地滿足這個需要,如:

class Employee: pass   john = Employee() # 生成一個空職員記錄  # 填充記錄的各個域 john.name = 'John Doe' john.dept = 'computer lab' john.salary = 1000


一段需要以某種抽象資料型別作為輸入的Python程式經常可以接受一個類作為輸入,該類只是模仿了應輸入的資料型別的方法。例如,如果你有一個函式是用來格式化一個檔案物件中的資料,就可一個定義一個具有方法read()和readline()的類,該類可以不從檔案輸入而是從一個字串緩衝區輸入,把這個類作為自變數。

 例項方法物件也有屬性:m.im_self是方法所屬的例項,m.im_func是方法對應的函式物件。 

9.7.1 例外可以是類

使用者自定義的例外除了可以是字串物件以外還可以是類。這樣可以定義可擴充的分層的類例外結構。

raise語句有兩種新的有效格式:

raise 類, 例項  raise 例項


在第一種形式中,“例項”必須是“類”的例項或“類”的匯出類的例項。第二種形式是

raise instance.__class__, instance


的簡寫。except語句除了可以列出字串物件外也可以列出類。execpt子句中列出的類如果是發生的例外類或基類則是匹配的(反過來不對——except中如果是匯出類而發生的例外屬於基類時是不匹配的)。例如,下面的程式會顯示B、C、D:

class B: pass class C(B): pass class D(C): pass   for c in [B, C, D]: try: raise c() except D: print "D" except C: print "C" except B: print "B"


注意如果把except子句的次序顛倒過來的話(“except B”放在最前),程式將顯示B,B ,B——因為第一個匹配的except子句被引發。

當沒有處理的例外是類的時候,類名顯示在錯誤資訊中,後面跟著一個冒號和一個空格,最後是例項用內建函式str()轉換成字串的結果。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-989439/,如需轉載,請註明出處,否則將追究法律責任。

相關文章