Python入門第9/10頁
更新時間:2007年02月08日 00:00:00 作者:
第九章 類
Python是一個真正面向?qū)ο蟮恼Z言,它只增加了很少的新語法就實現(xiàn)了類。它的類機制是C++ 和Modula-3的類機制的混合。Python的類并不嚴格限制用戶對定義的修改,它依賴于用戶自覺不去修改定義。然而Python對類最重要的功能都保持了完全的威力。類繼承機制允許多個基類的繼承,導(dǎo)出類可以重載基類的任何方法,方法可以調(diào)用基類的同名方法。對象可以包含任意多的私有數(shù)據(jù)。
用C++術(shù)語說,所有類成員(包括數(shù)據(jù)成員)是公用的,所有成員函數(shù)是虛擬(virtual)的。沒有特別的構(gòu)建函數(shù)或銷毀函數(shù)(destructor)。如同在Modula-3中一樣,從對象的方法中要引用對象成員沒有簡捷的辦法:方法函數(shù)的必須以對象作為第一個參數(shù),而在調(diào)用時則自動提供。象在Smalltalk中一樣,類本身也是對象,實際上這里對象的含義比較寬:在Python 中所有的數(shù)據(jù)類型都是對象。象在C++或Modula-3中一樣,內(nèi)置類型不能作為基類由用戶進行擴展。并且,象C++但不象Modula-3,多數(shù)有特殊語法的內(nèi)置函數(shù)(如算術(shù)算符、下標等)可以作為類成員重定義。
9.1 關(guān)于術(shù)語
Python的對象概念比較廣泛,對象不一定非得是類的實例,因為如同C++和Modula-3而不同于Smalltalk,Python的數(shù)據(jù)類型不都是類,比如基本內(nèi)置類型整數(shù)、列表等不是類,甚至較古怪的類型如文件也不是類。然而,Python所有的數(shù)據(jù)類型都或多或少地帶有一些類似對象的語法。
對象是有單獨身份的,同一對象可以有多個名字與其聯(lián)系,這在其他語言中叫做別名。這樣做的好處乍一看并不明顯,而且對于非可變類型(數(shù)字、字符串、序表(tuple))等沒有什么差別。但是別名句法對于包含可變對象如列表、字典及涉及程序外部物件如文件、窗口的程序有影響,這可以有利于程序編制,因為別名有些類似指針:比如,傳遞一個對象變得容易,因為這只是傳遞了一個指針;如果一個函數(shù)修改了作為參數(shù)傳遞來的對象,修改結(jié)果可以傳遞回調(diào)用處。這樣就不必象Pascal那樣使用兩種參數(shù)傳遞機制。
9.2 Python作用域與名字空間
在引入類之前,我們必須講一講Python的作用域規(guī)則。類定義很好地利用了名字空間,需要了解Python如何處理作用域和名字空間才能充分理解類的使用。另外,作用域規(guī)則也是一個高級Python程序員必須掌握的知識。
先給出一些定義。
名字空間是從名字到對象的映射。多數(shù)名字空間目前是用Python字典類型實現(xiàn)的,不過這一點一般是注意不到的,而且將來可能會改變。下面是名字空間的一些實例:Python中內(nèi)置的名字(如abs()等函數(shù),以及內(nèi)置的例外名);模塊中的全局名;函數(shù)調(diào)用中的局部變量名。在某種意義上一個對象的所有屬性也構(gòu)成了一個名字空間。關(guān)于名字空間最重要的事要知道不同名字空間的名字沒有任何聯(lián)系;例如,兩個不同模塊可能都定義了一個叫“maximize ”的函數(shù)而不會引起混亂,因為模塊的用戶必須在函數(shù)名之前加上模塊名作為修飾。
另外,在Python中可以把任何一個在句點之后的名字稱為屬性,例如,在表達式z.real中,real是一個對象z的屬性。嚴格地說,對模塊中的名字的引用是屬性引用:在表達式modname.funcname 中,modname是一個模塊對象,funcname是它的一個屬性。在這種情況下在模塊屬性與模塊定義的全局名字之間存在一個直接的映射:它們使用相同的名字空間!
屬性可以是只讀的也可以是可寫的。在屬性可寫的時候,可以對屬性賦值。模塊屬性是可寫的:你可以寫“modname.the_answer = 42”??蓪憣傩砸部梢杂胐el語句閃出,如“del modname.the_answer”。
名字空間與不同時刻創(chuàng)建,有不同的生存周期。包含Python內(nèi)置名字的名字空間當Python 解釋程序開始時被創(chuàng)建,而且不會被刪除。模塊的全局名字空間當模塊定義被讀入時創(chuàng)建,一般情況下模塊名字空間也一直存在到解釋程序退出。由解釋程序的最頂層調(diào)用執(zhí)行的語句,不論是從一個腳本文件讀入的還是交互輸入的,都屬于一個叫做__main__的模塊,所以也存在于自己的全局名字空間之中。(內(nèi)置名字實際上也存在于一個模塊中,這個模塊叫做__builtin__ )。
函數(shù)的局部名字空間當函數(shù)被調(diào)用時創(chuàng)建,當函數(shù)返回或者產(chǎn)生了一個不能在函數(shù)內(nèi)部處理的例外時被刪除。(實際上,說是忘記了這個名字空間更符合實際發(fā)生的情況。)當然,遞歸調(diào)用在每次遞歸中有自己的局部名字空間。
一個作用域是Python程序中的一個文本區(qū)域,其中某個名字空間可以直接訪問。“直接訪問” 這里指的是使用不加修飾的名字就直接找到名字空間中的對象。
雖然作用域是靜態(tài)定義的,在使用時作用域是動態(tài)的。在任何運行時刻,總是恰好有三個作用域在使用中(即恰好有三個名字空間是直接可訪問的):最內(nèi)層的作用域,最先被搜索,包含局部名字;中層的作用域,其次被搜索,包含當前模塊的全局名字;最外層的作用域最后被搜索,包含內(nèi)置名字。
一般情況下,局部作用域引用當前函數(shù)的局部名字,其中局部是源程序文本意義上來看的。在函數(shù)外部,局部作用域與全局作用域使用相同的名字空間:模塊的名字空間。類定義在局部作用域中又增加了另一個名字空間。
一定要注意作用域是按照源程序中的文本位置確定的:模塊中定義的函數(shù)的全局作用域是模塊的名字空間,不管這個函數(shù)是從哪里調(diào)用或者以什么名字調(diào)用的。另一方面,對名字的搜索卻是在程序運行中動態(tài)進行的,不過,Python語言的定義也在演變,將來可能發(fā)展到靜態(tài)名字解析,在“編譯”時,所以不要依賴于動態(tài)名字解析?。▽嶋H上,局部名字已經(jīng)是靜態(tài)確定的了)。
Python的一個特別之處是賦值總是進入最內(nèi)層作用域。關(guān)于刪除也是這樣:“del x”從局部作用域?qū)?yīng)的名字空間中刪除x的名字綁定(注意在Python中可以多個名字對應(yīng)一個對象,所以刪除一個名字只是刪除了這個名字與其對象間的聯(lián)系而不一定刪除這個對象。實際上,所有引入新名字的操作都使用局部作用域:特別的,import語句和函數(shù)定義把模塊名或函數(shù)名綁定入局部作用域。(可以使用global語句指明某些變量是屬于全局名字空間的)。
9.3 初識類
類引入了一些新語法,三種新對象類型,以及一些新的語義。
9.3.1 類定義語法
類定義的最簡單形式如下:
class 類名:
<語句-1>
.
.
.
<語句-N>
如同函數(shù)定義(def語句)一樣,類定義必須先執(zhí)行才能生效。(甚至可以把類定義放在if 語句的一個分支中或函數(shù)中)。在實際使用時,類定義中的語句通常是函數(shù)定義,其它語句也是允許的,有時是有用的――我們后面會再提到這一點。類內(nèi)的函數(shù)定義通常具有一種特別形式的自變量表,專用于方法的調(diào)用約定――這一點也會在后面詳細討論。
進入類定義后,產(chǎn)生了一個新的名字空間,被用作局部作用域――于是,所有對局部變量的賦值進入這個新名字空間。特別地,函數(shù)定義把函數(shù)名與新函數(shù)綁定在這個名字空間。
當函數(shù)定義正常結(jié)束(從結(jié)尾退出)時,就生成了一個類對象。這基本上是將類定義生成的名字空間包裹而成的一個對象;我們在下一節(jié)會學(xué)到類對象的更多知識。原始的局部作用域(在進入類定義之前起作用的那個)被恢復(fù),類對象在這里被綁定到了類對象定義頭部所指定的名字。
9.3.2 類對象
類對象支持兩種操作:屬性引用和實例化。屬性引用的格式和Python中其它的屬性引用格式相同,即obj.name。有效的屬性名包括生成類對象時的類名字空間中所有的名字。所以,如果象下面這樣定義類:
class MyClass:
"A simple example class"
i = 12345
def f(x):
return 'hello world'
則MyClass.i和MyClass.f都是有效的屬性引用,分別返回一個整數(shù)和一個函數(shù)對象。也可以對類屬性賦值,所以你可以對MyClass.i賦值而改變該屬性的值。
__doc__也是一個有效的屬性,它是只讀的,返回類的文檔字符串:“A simple example class”。
類實例化使用函數(shù)記號。只要把這個類對象看成是一個沒有自變量的函數(shù),返回一個類實例。例如(假設(shè)使用上面的類):
x = MyClass()
可以生成該類的一個新實例并把實例對象賦給局部變量x?!?nbsp;
9.3.3 實例對象
我們?nèi)绾问褂脤嵗龑ο竽??類實例只懂得屬性引用這一種操作。有兩類有效的屬性。
第一類屬性叫做數(shù)據(jù)屬性。數(shù)據(jù)屬性相當于Smalltalk中的“實例變量”,和C++中的“數(shù)據(jù)成員”。數(shù)據(jù)成員不需要聲明,也不需要在類定義中已經(jīng)存在,象局部變量一樣,只要一賦值它就產(chǎn)生了。例如,如果x是上面的MyClass類的一個實例,則下面的例子將顯示值16而不會留下任何痕跡:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print x.counter
del x.counter
類實例能理解的第二類屬性引用是方法。方法是“屬于”一個對象的函數(shù)。(在Python中,方法并不是只用于類實例的:其它對象類型也可以有方法,例如,列表對象也有append、insert 、remove、sort等方法。不過,在這里除非特別說明我們用方法來特指類實例對象的方法)。
類對象的有效方法名依賴于它的類。按照定義,類的所有類型為函數(shù)對象屬性定義了其實例的對應(yīng)方法。所以在我們的例子y,x.f是一個有效的方法引用,因為MyClass是一個函數(shù);x.i 不是方法引用,因為MyClass.i不是。但是x.f和MyClass.f不是同一個東西――x.f是一個方法對象而不是一個函數(shù)對象。
9.3.4 方法對象
方法一般是直接調(diào)用的,例如:
x.f()
在我們的例子中,這將返回字符串‘hello world'。然而,也可以不直接調(diào)用方法:x.f 是一個方法對象,可以把它保存起來再調(diào)用。例如:
xf = x.f
while 1:
print xf()
會不停地顯示“hello world”?!?nbsp;
調(diào)用方法時到底發(fā)生了什么呢?你可能已經(jīng)注意到x.f()調(diào)用沒有自變量,而函數(shù)f在調(diào)用時有一個自變量。那個自變量是怎么回事?Python如果調(diào)用一個需要自變量的函數(shù)時忽略自變量肯定會產(chǎn)生例外錯誤――即使那個自變量不需要用到……
實際上,你可以猜出答案:方法與函數(shù)的區(qū)別在于對象作為方法的第一個自變量自動傳遞給方法。在我們的例子中,調(diào)用x.f()等價于調(diào)用MyClass.f(x)。一般地,用n個自變量的去調(diào)用方法等價于把方法所屬對象插入到第一個自變量前面以后調(diào)用對應(yīng)函數(shù)。
如果你還不理解方法是如何工作的,看一看方法的實現(xiàn)可能會有所幫助。在引用非數(shù)據(jù)屬性的實例屬性時,將搜索它的類。如果該屬性名是一個有效的函數(shù)對象,就生成一個方法對象,把實例對象(的指針)和函數(shù)對象包裝到一起:這就是方法對象。當方法對象用一個自變量表調(diào)用時,它再被打開包裝,由實例對象和原自變量表組合起來形成新自變量表,用這個新自變量表調(diào)用函數(shù)。
9.4 一些說明
在名字相同時數(shù)據(jù)屬性會覆蓋方法屬性;為了避免偶然的名字沖突,這在大型程序中會造成難以查找的錯誤,最好按某種命名慣例來區(qū)分方法名和數(shù)據(jù)名,例如,所有方法名用大寫字母開頭,所有數(shù)據(jù)屬性名前用一個唯一的字符串開頭(或者只是一個下劃線),或方法名用動詞而數(shù)據(jù)名用名詞。
數(shù)據(jù)屬性可以被方法引用也可以被普通用戶(“客戶”)引用。換句話說,類不能用來構(gòu)造抽象數(shù)據(jù)類型。實際上,Python中沒有任何辦法可以強制進行數(shù)據(jù)隱藏——這些都是基于慣例。(另一方面,Python的實現(xiàn)是用C寫的,它可以完全隱藏實現(xiàn)細節(jié),必要時可以控制對象存??;用C寫的Python擴展模塊也有同樣特性)。
客戶要自己小心使用數(shù)據(jù)屬性——客戶可能會因為隨意更改類對象的數(shù)據(jù)屬性而破壞由類方法維護的類數(shù)據(jù)的一致性。注意客戶只要注意避免名字沖突可以任意為實例對象增加新數(shù)據(jù)屬性而不需影響到方法的有效性——這里,有效的命名慣例可以省去許多麻煩。
從方法內(nèi)要訪問本對象的數(shù)據(jù)屬性(或其它方法)沒有一個簡寫的辦法。我認為這事實上增加了程序的可讀性:在方法定義中不會混淆局部變量和實例變量。
習(xí)慣上,方法的第一自變量叫做self。這只不過是一個習(xí)慣用法:名字self在Python中沒有任何特殊意義。但是,因為用戶都使用此慣例,所以違背此慣例可能使其它Python程序員不容易讀你的程序,可以想象某些類瀏覽程序會依賴于此慣例)。
作為類屬性的任何函數(shù)對象都為該類的實例定義一個方法。函數(shù)的定義不一定必須在類定義內(nèi)部:只要在類內(nèi)把一個函數(shù)對象賦給一個局部變量就可以了。例如:
# Function defined outside the class
def f1(self, x, y):
return min(x, x+y)
class C:
f = f1
def g(self):
return 'hello world'
h = g
現(xiàn)在f、g和h都是類C的屬性且指向函數(shù)對象,所以它們都是C的實例的方法——其中h與g 完全等價。注意我們應(yīng)該避免這種用法以免誤導(dǎo)讀者。
方法可以用代表所屬對象的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)
實例化操作(“調(diào)用”一個類對象)生成一個空對象。許多類要求生成具有已知初識狀態(tài)的類。為此,類可以定義一個特殊的叫做__init__()的方法,如:
def __init__(self):
self.empty()
一個類定義了__init__()方法以后,類實例化時就會自動為新生成的類實例調(diào)用調(diào)用__init__() 方法。所以在Bag例子中,可以用如下程序生成新的初始化的實例:
x = Bag()
當然,__init__()方法可以有自變量,這樣可以實現(xiàn)更大的靈活性。在這樣的情況下,類實例化時指定的自變量被傳遞給__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)
方法可以和普通函數(shù)一樣地引用全局名字。方法的全局作用域是包含類定義的模塊。(注意類本身并不被用作全局作用域?。╇m然我們很少需要在方法中使用全局數(shù)據(jù),全局作用域還是有許多合法的用途:例如,導(dǎo)入全局作用域的函數(shù)和模塊可以被方法使用,在同一模塊中定義的函數(shù)和方法也可以被方法使用。包含此方法的類一般也在此全局作用域中定義,下一節(jié)我們會看到一個方法為什么需要引用自己的類!
9.5 繼承
當然,一個語言如果不支持繼承就談不到“類”。導(dǎo)出類的定義方法如下:
class 導(dǎo)出類名(基類名):
<語句-1>
.
.
.
<語句-N>
其中“基類名”必須在包含導(dǎo)出類定義的作用域中有定義。除了給出基類名外,還可以給出一個表達式,在基類定義于其它模塊中時這是有用的,如:
class 導(dǎo)出類名 (模塊名.基類名):
導(dǎo)出類定義的運行和基類運行的方法是一樣的。生成類對象是,基類被記憶。這用于解決屬性引用:如果類中未找到要求的屬性就到基類中去查找。如果基類還有基類的話這個規(guī)則遞歸地應(yīng)用到更高的類。
導(dǎo)出類在實例化時沒有任何特殊規(guī)則?!皩?dǎo)出類名()”產(chǎn)生該類的一個新實例。方法引用這樣解決:搜索相應(yīng)類屬性,如果必要的話逐級向基類查找,如果找到了一個函數(shù)對象就是有效的方法引用。
導(dǎo)出類可以重寫基類的方法。因為方法在調(diào)用同一對象的其它方法時并無任何特殊權(quán)限,如果基類中某一方法調(diào)用同一基類的另一方法,在導(dǎo)出類中該方法調(diào)用的就可能是已經(jīng)被導(dǎo)出類重寫后的方法了。(對C++程序員而言:Python中所有方法都是“虛擬函數(shù)”)。
導(dǎo)出類中重寫的方法可能是需要擴充基類的同名方法而不是完全代替原來的方法。導(dǎo)出類調(diào)用基類同名方法很簡單:“基類名.方法名(self, 自變量表)”。對類用戶這種做法偶爾也是有用的。(注意只有基類在同一全局作用域定義或?qū)霑r才能這樣用)。
8.5.1 多重繼承
Python也支持有限的多重繼承。有多個基類的類定義格式如下:
class 導(dǎo)出類名 (基類1, 基類2, 基類3):
<語句-1>
.
.
.
<語句-N>
關(guān)于多重繼承只需要解釋如何解決類屬性引用。類屬性引用是深度優(yōu)先,從左向右進行的。所以,如果在導(dǎo)出類定義中未找到某個屬性,就先在基類1中查找,然后(遞歸地)在基類1 的基類中查找,如果都沒有找到,就在基類2中查找,如此進行下去。
(對某些人來說寬度優(yōu)先——先在基類2和基類3中查找再到基類1的基類中查找——看起來更自然。然而,這需要你在確定基類1與基類2的屬性沖突時明確知道這個屬性是在基類1本身定義還是在其基類中定義。深度優(yōu)先規(guī)則不區(qū)分基類1的一個屬性到底是直接定義的還是繼承來的)。
很顯然,如果不加約束地使用多重繼承會造成程序維護的惡夢,因為Python避免名字沖突只靠習(xí)慣約定。多重繼承的一個眾所周知的問題是當導(dǎo)出類有兩個基類恰好從同一個基類導(dǎo)出的。盡管很容易想到這種情況的后果(實例只有一份“實例變量”或數(shù)據(jù)屬性被共同的基類使用),但是這種做法有什么用處卻是不清楚的。
9.6 私有變量
Python對私有類成員有部分支持。任何象__spam這樣形式的標識符(至少有兩個前導(dǎo)下劃線,至多有一個結(jié)尾下劃線)目前被替換成_classname__spam,其中classname是所屬類名去掉前導(dǎo)下劃線的結(jié)果。這種攪亂不管標識符的語法位置,所以可以用來定義類私有的實例、變量、方法,以及全局變量,甚至于保存對于此類是私有的其它類的實例。如果攪亂的名字超過255個字符可能會發(fā)生截斷。在類外面或類名只有下劃線時不進行攪亂。
名字攪亂的目的是給類一種定義“私有”實例變量和方法的簡單方法,不需擔(dān)心它的其它類會定義同名變量,也不怕類外的代碼弄亂實例的變量。注意攪亂規(guī)則主要是為了避免偶然的錯誤,如果你一定想做的話仍然可以訪問或修改私有變量。這甚至是有用的,比如調(diào)試程序要用到私有變量,這也是為什么這個漏洞沒有堵上的一個原因。(小錯誤:導(dǎo)出類和基類取相同的名字就可以使用基類的私有變量)。
注意傳遞給exec,eval()或evalfile()的代碼不會認為調(diào)用它們的類的類名是當前類,這與global語句的情況類似,global的作用局限于一起字節(jié)編譯的代碼。同樣的限制也適用于getattr() ,setattr()和delattr(),以及直接訪問__dict__的時候。
下面例子中的類實現(xiàn)了自己的__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”的類型,可以把幾個有名的數(shù)據(jù)項組合在一起。一個空類可以很好地滿足這個需要,如:
class Employee:
pass
john = Employee() # 生成一個空職員記錄
# 填充記錄的各個域
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
一段需要以某種抽象數(shù)據(jù)類型作為輸入的Python程序經(jīng)??梢越邮芤粋€類作為輸入,該類只是模仿了應(yīng)輸入的數(shù)據(jù)類型的方法。例如,如果你有一個函數(shù)是用來格式化一個文件對象中的數(shù)據(jù),就可一個定義一個具有方法read()和readline()的類,該類可以不從文件輸入而是從一個字符串緩沖區(qū)輸入,把這個類作為自變量。
實例方法對象也有屬性:m.im_self是方法所屬的實例,m.im_func是方法對應(yīng)的函數(shù)對象。
9.7.1 例外可以是類
用戶自定義的例外除了可以是字符串對象以外還可以是類。這樣可以定義可擴充的分層的類例外結(jié)構(gòu)。
raise語句有兩種新的有效格式:
raise 類, 實例
raise 實例
在第一種形式中,“實例”必須是“類”的實例或“類”的導(dǎo)出類的實例。第二種形式是
raise instance.__class__, instance
的簡寫。except語句除了可以列出字符串對象外也可以列出類。execpt子句中列出的類如果是發(fā)生的例外類或基類則是匹配的(反過來不對——except中如果是導(dǎo)出類而發(fā)生的例外屬于基類時是不匹配的)。例如,下面的程序會顯示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”放在最前),程序?qū)@示B,B ,B——因為第一個匹配的except子句被引發(fā)。
當沒有處理的例外是類的時候,類名顯示在錯誤信息中,后面跟著一個冒號和一個空格,最后是實例用內(nèi)置函數(shù)str()轉(zhuǎn)換成字符串的結(jié)果。
相關(guān)文章
Python 中下劃線的幾種用法(_、_xx、xx_、__xx、__xx__)
本文主要介紹了Python 中下劃線的幾種用法(_、_xx、xx_、__xx、__xx__),詳細的介紹了這幾種下劃線的用處,具有一定的參考價值,感興趣的可以了解一下2023-09-09Python 提取dict轉(zhuǎn)換為xml/json/table并輸出的實現(xiàn)代碼
這篇文章主要介紹了Python 提取dict轉(zhuǎn)換為xml/json/table并輸出的實現(xiàn)代碼,需要的朋友可以參考下2016-08-08Python selenium+cookie實現(xiàn)免密登陸的示例代碼
本文主要介紹了Python selenium+cookie實現(xiàn)免密登陸的示例代碼,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02