鍍金池/ 教程/ Python/ 類(lèi)
浮點(diǎn)運(yùn)算
非正式的 Python 簡(jiǎn)介
深入流程控制
標(biāo)準(zhǔn)庫(kù)概覽
交互式輸入行編輯歷史回溯
輸入和輸出
使用 Python 解釋器
標(biāo)準(zhǔn)庫(kù)概覽Ⅱ
錯(cuò)誤和異常
模塊
數(shù)據(jù)結(jié)構(gòu)
附錄
接下來(lái)?
類(lèi)
激起你的興趣

類(lèi)

與其他編程語(yǔ)言相比,Python 的類(lèi)機(jī)制通過(guò)最小的新語(yǔ)法和語(yǔ)義在語(yǔ)言中實(shí)現(xiàn)了類(lèi)。 它是 C++ 或者 Modula-3 語(yǔ)言中類(lèi)機(jī)制的混合。Python 類(lèi)提供所有標(biāo)準(zhǔn)的面向?qū)ο缶幊坦δ?允許多個(gè)基類(lèi)的類(lèi)繼承機(jī)制,派生類(lèi)可以覆蓋基類(lèi)的任何方法或類(lèi),一個(gè)方法可以調(diào)用與基類(lèi)方法名字相同的方法。對(duì)象可以包含任意數(shù)量和種類(lèi)的數(shù)據(jù)。對(duì)于正確的模塊、類(lèi)參加 Python 的動(dòng)態(tài)特性在運(yùn)行時(shí)創(chuàng)建。它們創(chuàng)建后可以進(jìn)一步修改。

用 C++ 術(shù)語(yǔ)來(lái)講,所有的類(lèi)成員(包括數(shù)據(jù)成員)都是公有(?public)的,所有的成員函數(shù)都是虛(?virtual?)的。用 Modula-3 的術(shù)語(yǔ)來(lái)講,在成員方法中沒(méi)有簡(jiǎn)便的方式引用對(duì)象的成員:方法函數(shù)在定義時(shí)需要以引用的對(duì)象做為第一個(gè)參數(shù),調(diào)用時(shí)則會(huì)隱式引用對(duì)象。像在 Smalltalk 中一個(gè),類(lèi)也是對(duì)象。這就提供了導(dǎo)入和重命名語(yǔ)義。不像 C++ 和 Modula-3 中那樣,大多數(shù)帶有特殊語(yǔ)法的內(nèi)置操作符(算法運(yùn)算符、下標(biāo)等)都可以針對(duì)類(lèi)的需要重新定義。

(在討論類(lèi)時(shí),沒(méi)有足夠的得到共識(shí)的術(shù)語(yǔ),我會(huì)偶爾從 Smalltalk 和 C++ 借用一些。我比較喜歡用 Modula-3 的用語(yǔ),因?yàn)楸绕?C++, Python 的面向?qū)ο笳Z(yǔ)法更像它,但是我想很少有讀者聽(tīng)過(guò)這個(gè)。)

術(shù)語(yǔ)相關(guān)

對(duì)象具有特性,并且多個(gè)名稱(chēng)(在多個(gè)作用于中)可以綁定在同一個(gè)對(duì)象上。 這在其它語(yǔ)言中被稱(chēng)為別名。 在對(duì) Python 的第一印象中這通常會(huì)被忽略,并且當(dāng)處理不可變基礎(chǔ)類(lèi)型(數(shù)字,字符串,元組)時(shí)可以被放心的忽略。 但是,在調(diào)用列表、字典這類(lèi)可變對(duì)象,或者大多數(shù)程序外部類(lèi)型(文件,窗體等)描述實(shí)體時(shí),別名對(duì) Python 代碼的語(yǔ)義便具有(有意而為?。┯绊憽?這通常有助于程序的優(yōu)化,因?yàn)樵谀承┓矫鎰e名表現(xiàn)的就像是指針。 例如,你可以輕易的傳遞一個(gè)對(duì)象,因?yàn)橥ㄟ^(guò)繼承只是傳遞一個(gè)指針。 并且如果一個(gè)方法修改了一個(gè)作為參數(shù)傳遞的對(duì)象,調(diào)用者可以接收這一變化——這消除了兩種不同的參數(shù)傳遞機(jī)制的需要,像 Pascal 語(yǔ)言。

作用域和命名空間

在介紹類(lèi)之前,我首先介紹一些有關(guān) Python 作用域的規(guī)則。類(lèi)的定義非常巧妙的運(yùn)用了命名空間,要完全理解接下來(lái)的知識(shí),需要先理解作用域和命名空間的工作原理。另外,這一切的知識(shí)對(duì)于任何高級(jí) Python 程序員都非常有用。

讓我們從一些定義說(shuō)起。

命名空間是從命名到對(duì)象的映射。當(dāng)前命名空間主要是通過(guò) Python 字典實(shí)現(xiàn)的,不過(guò)通常不關(guān)心具體的實(shí)現(xiàn)方式(除非出于性能考慮),以后也有可能會(huì)改變其實(shí)現(xiàn)方式。以下有一些命名空間的例子:內(nèi)置命名(像?abs()?這樣的函數(shù),以及內(nèi)置異常名)集,模塊中的全局命名,函數(shù)調(diào)用中的局部命名。某種意義上講對(duì)象的屬性集也是一個(gè)命名空間。關(guān)于命名空間需要了解的一件很重要的事就是不同命名空間中的命名沒(méi)有任何聯(lián)系,例如兩個(gè)不同的模塊可能都會(huì)定義一個(gè)名為?maximize?的函數(shù)而不會(huì)發(fā)生混淆--用戶必須以模塊名為前綴來(lái)引用它們。

順便提一句,我稱(chēng) Python 中任何一個(gè)“.”之后的命名為?屬性?--例如,表達(dá)式?z.real?中的?real?是對(duì)象?z?的一個(gè)屬性。嚴(yán)格來(lái)講,從模塊中引用命名是引用屬性:表達(dá)式?modname.funcname?中,?modname?是一個(gè)模塊對(duì)象,funcname?是它的一個(gè)屬性。因此,模塊的屬性和模塊中的全局命名有直接的映射關(guān)系:它們共享同一命名空間![1]

屬性可以是只讀過(guò)或?qū)懙摹:笠环N情況下,可以對(duì)屬性賦值。你可以這樣作:?modname.the_answer?=?42???蓪?xiě)的屬性也可以用?del?語(yǔ)句刪除。例如:?del?modname.the_answer?會(huì)從?modname?對(duì)象中刪除?the_answer?屬性。

不同的命名空間在不同的時(shí)刻創(chuàng)建,有不同的生存期。包含內(nèi)置命名的命名空間在 Python 解釋器啟動(dòng)時(shí)創(chuàng)建,會(huì)一直保留,不被刪除。模塊的全局命名空間在模塊定義被讀入時(shí)創(chuàng)建,通常,模塊命名空間也會(huì)一直保存到解釋器退出。由解釋器在最高層調(diào)用執(zhí)行的語(yǔ)句,不管它是從腳本文件中讀入還是來(lái)自交互式輸入,都是?__main__?模塊的一部分,所以它們也擁有自己的命名空間。(內(nèi)置命名也同樣被包含在一個(gè)模塊中,它被稱(chēng)作?__builtin__?。)

當(dāng)調(diào)用函數(shù)時(shí),就會(huì)為它創(chuàng)建一個(gè)局部命名空間,并且在函數(shù)返回或拋出一個(gè)并沒(méi)有在函數(shù)內(nèi)部處理的異常時(shí)被刪除。 (實(shí)際上,用遺忘來(lái)形容到底發(fā)生了什么更為貼切。) 當(dāng)然,每個(gè)遞歸調(diào)用都有自己的局部命名空間。

作用域?就是一個(gè) Python 程序可以直接訪問(wèn)命名空間的正文區(qū)域。 這里的 直接訪問(wèn) 意思是一個(gè)對(duì)名稱(chēng)的錯(cuò)誤引用會(huì)嘗試在命名空間內(nèi)查找。

盡管作用域是靜態(tài)定義,在使用時(shí)他們都是動(dòng)態(tài)的。每次執(zhí)行時(shí),至少有三個(gè)命名空間可以直接訪問(wèn)的作用域嵌套在一起:

  • 包含局部命名的使用域在最里面,首先被搜索;其次搜索的是中層的作用域,這里包含了同級(jí)的函數(shù);最后搜索最外面的作用域,它包含內(nèi)置命名。
  • 首先搜索最內(nèi)層的作用域,它包含局部命名任意函數(shù)包含的作用域,是內(nèi)層嵌套作用域搜索起點(diǎn),包含非局部,但是也非全局的命名
  • 接下來(lái)的作用域包含當(dāng)前模塊的全局命名
  • 最外層的作用域(最后搜索)是包含內(nèi)置命名的命名空間。

如果一個(gè)命名聲明為全局的,那么所有的賦值和引用都直接針對(duì)包含模全局命名的中級(jí)作用域。另外,從外部訪問(wèn)到的所有內(nèi)層作用域的變量都是只讀的。(試圖寫(xiě)這樣的變量只會(huì)在內(nèi)部作用域創(chuàng)建一個(gè)?新?局部變量,外部標(biāo)示命名的那個(gè)變量不會(huì)改變)。

通常,局部作用域引用當(dāng)前函數(shù)的命名。在函數(shù)之外,局部作用域與全局使用域引用同一命名空間:模塊命名空間。類(lèi)定義也是局部作用域中的另一個(gè)命名空間。

重要的是作用域決定于源程序的意義:一個(gè)定義于某模塊中的函數(shù)的全局作用域是該模塊的命名空間,而不是該函數(shù)的別名被定義或調(diào)用的位置,了解這一點(diǎn)非常重要。另一方面,命名的實(shí)際搜索過(guò)程是動(dòng)態(tài)的,在運(yùn)行時(shí)確定的——然而,Python 語(yǔ)言也在不斷發(fā)展,以后有可能會(huì)成為靜態(tài)的“編譯”時(shí)確定,所以不要依賴(lài)動(dòng)態(tài)解析?。ㄊ聦?shí)上,局部變量已經(jīng)是靜態(tài)確定了。)

Python 的一個(gè)特別之處在于——如果沒(méi)有使用?global?語(yǔ)法——其賦值操作總是在最里層的作用域。賦值不會(huì)復(fù)制數(shù)據(jù)——只是將命名綁定到對(duì)象。刪除也是如此:?del?x?只是從局部作用域的命名空間中刪除命名 x?。事實(shí)上,所有引入新命名的操作都作用于局部作用域。特別是import?語(yǔ)句和函數(shù)定將模塊名或函數(shù)綁定于局部作用域。(可以使用 global?語(yǔ)句將變量引入到全局作用域。)

global?語(yǔ)句用以指明某個(gè)特定的變量為全局作用域,并重新綁定它。nonlocal?語(yǔ)句用以指明某個(gè)特定的變量為封閉作用域,并重新綁定它。

作用域和命名空間示例

以下是一個(gè)示例,演示了如何引用不同作用域和命名空間,以及?global 和?nonlocal?如何影響變量綁定:

Def scope_test():  
    def do_local():  
        spam = "local spam"  
    def do_nonlocal():  
        nonlocal spam  
        spam = "nonlocal spam"  
    def do_global():  
        global spam  
        spam = "global spam"  
    spam = "test spam"  
    do_local()  
    print("After local assignment:", spam)  
    do_nonlocal()  
    print("After nonlocal assignment:", spam)  
    do_global()  
    print("After global assignment:", spam)

scope_test()  
print("In global scope:", spam)  

以上示例代碼的輸出為:

After local assignment: test spam  
After nonlocal assignment: nonlocal spam  
After global assignment: nonlocal spam  
In global scope: global spam

注意:?local?賦值語(yǔ)句是無(wú)法改變?scope_test?的?spam?綁定。?nonlocal賦值語(yǔ)句改變了?scope_test?的?spam?綁定,并且?global?賦值語(yǔ)句從模塊級(jí)改變了 spam 綁定。 你也可以看到在?global?賦值語(yǔ)句之前對(duì) spam 是沒(méi)有預(yù)先綁定的。

初識(shí)類(lèi)

類(lèi)引入了一些新語(yǔ)法:三種新的對(duì)象類(lèi)型和一些新的語(yǔ)義。

類(lèi)定義語(yǔ)法

類(lèi)定義最簡(jiǎn)單的形式如下:

class ClassName:  
    <statement-1>  
    .  
    .  
    .  
    <statement-N>  

類(lèi)的定義就像函數(shù)定義(?def?語(yǔ)句),要先執(zhí)行才能生效。(你當(dāng)然可以把它放進(jìn)?if?語(yǔ)句的某一分支,或者一個(gè)函數(shù)的內(nèi)部。)

習(xí)慣上,類(lèi)定義語(yǔ)句的內(nèi)容通常是函數(shù)定義,不過(guò)其它語(yǔ)句也可以,有時(shí)會(huì)很有用——后面我們?cè)倩剡^(guò)頭來(lái)討論。類(lèi)中的函數(shù)定義通常包括了一個(gè)特殊形式的參數(shù)列表,用于方法調(diào)用約定——同樣我們?cè)诤竺嬗懻撨@些。

進(jìn)入類(lèi)定義部分后,會(huì)創(chuàng)建出一個(gè)新的命名空間,作為局部作用域——因此,所有的賦值成為這個(gè)新命名空間的局部變量。特別是函數(shù)定義在此綁定了新的命名。

類(lèi)定義完成時(shí)(正常退出),就創(chuàng)建了一個(gè)?類(lèi)對(duì)象?。基本上它是對(duì)類(lèi)定義創(chuàng)建的命名空間進(jìn)行了一個(gè)包裝;我們?cè)谙乱还?jié)進(jìn)一步學(xué)習(xí)類(lèi)對(duì)象的知識(shí)。原始的局部作用域(類(lèi)定義引入之前生效的那個(gè))得到恢復(fù),類(lèi)對(duì)象在這里綁定到類(lèi)定義頭部的類(lèi)名(例子中是?ClassName?)。

類(lèi)對(duì)象

類(lèi)對(duì)象支持兩種操作:屬性引用和實(shí)例化。

屬性引用?使用和 Python 中所有的屬性引用一樣的標(biāo)準(zhǔn)語(yǔ)法:obj.name。類(lèi)對(duì)象創(chuàng)建后,類(lèi)命名空間中所有的命名都是有效屬性名。所以如果類(lèi)定義是這樣:

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

那么?MyClass.i?和?MyClass.f?是有效的屬性引用,分別返回一個(gè)整數(shù)和一個(gè)方法對(duì)象。也可以對(duì)類(lèi)屬性賦值,你可以通過(guò)給?MyClass.i?賦值來(lái)修改它。?__doc__?也是一個(gè)有效的屬性,返回類(lèi)的文檔字符串:"A?simple?example?class"?。

類(lèi)的實(shí)例化?使用函數(shù)符號(hào)。只要將類(lèi)對(duì)象看作是一個(gè)返回新的類(lèi)實(shí)例的無(wú)參數(shù)函數(shù)即可。例如(假設(shè)沿用前面的類(lèi)):

x = MyClass()

以上創(chuàng)建了一個(gè)新的類(lèi)?實(shí)例?并將該對(duì)象賦給局部變量?x?。

這個(gè)實(shí)例化操作(“調(diào)用”一個(gè)類(lèi)對(duì)象)來(lái)創(chuàng)建一個(gè)空的對(duì)象。很多類(lèi)都傾向于將對(duì)象創(chuàng)建為有初始狀態(tài)的。因此類(lèi)可能會(huì)定義一個(gè)名為__init__()?的特殊方法,像下面這樣:

def __init__(self):  
    self.data = []  

類(lèi)定義了?__init__()?方法的話,類(lèi)的實(shí)例化操作會(huì)自動(dòng)為新創(chuàng)建的類(lèi)實(shí)例調(diào)用?__init__()?方法。所以在下例中,可以這樣創(chuàng)建一個(gè)新的實(shí)例:

X = MyClass()

當(dāng)然,出于彈性的需要,?__init__()?方法可以有參數(shù)。事實(shí)上,參數(shù)通過(guò)?__init__()?傳遞到類(lèi)的實(shí)例化操作上。例如,

>>> 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í)例對(duì)象

現(xiàn)在我們可以用實(shí)例對(duì)象作什么?實(shí)例對(duì)象唯一可用的操作就是屬性引用。有兩種有效的屬性名。

數(shù)據(jù)屬性?相當(dāng)于 Smalltalk 中的“實(shí)例變量”或 C++ 中的“數(shù)據(jù)成員”。和局部變量一樣,數(shù)據(jù)屬性不需要聲明,第一次使用時(shí)它們就會(huì)生成。例如,如果?x?是前面創(chuàng)建的?MyClass?實(shí)例,下面這段代碼會(huì)打印出 16 而在堆棧中留下多余的東西:

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

另一種為實(shí)例對(duì)象所接受的引用屬性是?方法?。方法是“屬于”一個(gè)對(duì)象的函數(shù)。(在 Python 中,方法不止是類(lèi)實(shí)例所獨(dú)有:其它類(lèi)型的對(duì)象也可有方法。例如,鏈表對(duì)象有 append,insert,remove,sort 等等方法。然而,在后面的介紹中,除非特別說(shuō)明,我們提到的方法特指類(lèi)方法)

實(shí)例對(duì)象的有效名稱(chēng)依賴(lài)于它的類(lèi)。按照定義,類(lèi)中所有(用戶定義)的函數(shù)對(duì)象對(duì)應(yīng)它的實(shí)例中的方法。所以在我們的例子中,x.f?是一個(gè)有效的方法引用,因?yàn)?MyClass.f?是一個(gè)函數(shù)。但?x.i?不是,因?yàn)镸yClass.i?不是函數(shù)。不過(guò)?x.f?和?MyClass.f?不同--它是一個(gè)?方法對(duì)象?,不是一個(gè)函數(shù)對(duì)象。

方法對(duì)象

通常,方法通過(guò)右綁定方式調(diào)用:

x.f()

在?MyClass?示例中,這會(huì)返回字符串?'hello?world'?。然而,也不是一定要直接調(diào)用方法。?x.f?是一個(gè)方法對(duì)象,它可以存儲(chǔ)起來(lái)以后調(diào)用。例如:

Xf = x.f  
while True:  
   print(xf())  

會(huì)不斷的打印?hello?world?。

調(diào)用方法時(shí)發(fā)生了什么?你可能注意到調(diào)用?x.f()?時(shí)沒(méi)有引用前面標(biāo)出的變量,盡管在?f()?的函數(shù)定義中指明了一個(gè)參數(shù)。這個(gè)參數(shù)怎么了?事實(shí)上如果函數(shù)調(diào)用中缺少參數(shù),Python 會(huì)拋出異常--甚至這個(gè)參數(shù)實(shí)際上沒(méi)什么用……

實(shí)際上,你可能已經(jīng)猜到了答案:方法的特別之處在于實(shí)例對(duì)象作為函數(shù)的第一個(gè)參數(shù)傳給了函數(shù)。在我們的例子中,調(diào)用?x.f()?相當(dāng)于 MyClass.f(x)?。通常,以?n?個(gè)參數(shù)的列表去調(diào)用一個(gè)方法就相當(dāng)于將方法的對(duì)象插入到參數(shù)列表的最前面后,以這個(gè)列表去調(diào)用相應(yīng)的函數(shù)。

如果你還是不理解方法的工作原理,了解一下它的實(shí)現(xiàn)也許有幫助。引用非數(shù)據(jù)屬性的實(shí)例屬性時(shí),會(huì)搜索它的類(lèi)。 如果這個(gè)命名確認(rèn)為一個(gè)有效的函數(shù)對(duì)象類(lèi)屬性,就會(huì)將實(shí)例對(duì)象和函數(shù)對(duì)象封裝進(jìn)一個(gè)抽象對(duì)象:這就是方法對(duì)象。以一個(gè)參數(shù)列表調(diào)用方法對(duì)象時(shí),它被重新拆 封,用實(shí)例對(duì)象和原始的參數(shù)列表構(gòu)造一個(gè)新的參數(shù)列表,然后函數(shù)對(duì)象調(diào)用這個(gè)新的參數(shù)列表。

類(lèi)和實(shí)例變量

一般來(lái)說(shuō),實(shí)例變量數(shù)據(jù)對(duì)于每個(gè)實(shí)例是獨(dú)一無(wú)二的,類(lèi)變量的屬性和方法被類(lèi)的所有實(shí)例共享:

class Dog:  
    kind = 'canine'         # class variable shared by all instances  
    def __init__(self, name):  
        self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')  
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'  
>>> e.kind                  # shared by all dogs
'canine' 
>>> d.name                  # unique to d
'Fido'  
>>> e.name                  # unique to e
'Buddy'  

正如一句話所討論的關(guān)于名稱(chēng)和對(duì)象,共享數(shù)據(jù)可能會(huì)有驚人的效果伴隨著涉及的可變對(duì)象,比如列表和字典。例如,技巧列表下面的代碼不應(yīng)只作為一個(gè)類(lèi)變量,因?yàn)橐粋€(gè)列表將由所有狗實(shí)例共享:

class Dog:  

     tricks = []             # mistaken use of a class   variable 

     def __init__(self, name):  
         self.name = name  
     def add_trick(self, trick):  
         self.tricks.append(trick)

>>> d = Dog('Fido')  
>>> e = Dog('Buddy')  
>>> d.add_trick('roll over')  
>>> e.add_trick('play dead')  
>>> d.tricks                 # unexpectedly shared by all dogs  
['roll over', 'play dead']  

正確的類(lèi)的設(shè)計(jì)應(yīng)該使用一個(gè)實(shí)例變量,而不是:

class Dog:

    def __init__(self, name):  
        self.name = name  
        self.tricks = []    # creates a new empty list for each dog 

    def add_trick(self, trick):  
        self.tricks.append(trick)    
>>> d = Dog('Fido')  
>>> e = Dog('Buddy')  
>>> d.add_trick('roll over')  
>>> e.add_trick('play dead')  
>>> d.tricks
['roll over']  
>>> e.tricks  
['play dead']  

一些說(shuō)明

數(shù)據(jù)屬性會(huì)覆蓋同名的方法屬性。 為了避免意外的名稱(chēng)沖突,這在大型程序中是極難發(fā)現(xiàn)的 Bug,使用一些約定來(lái)減少?zèng)_突的機(jī)會(huì)是明智的。 可能的約定包括:大寫(xiě)方法名稱(chēng)的首字母,使用一個(gè)唯一的小字符串(也許只是一個(gè)下劃線)作為數(shù)據(jù)屬性名稱(chēng)的前綴,或者方法使用動(dòng)詞而數(shù)據(jù)屬性使用名詞。

數(shù)據(jù)屬性可以被方法引用,也可以由一個(gè)對(duì)象的普通用戶(客戶)使用。 換句話說(shuō),類(lèi)不能用來(lái)實(shí)現(xiàn)純凈的數(shù)據(jù)類(lèi)型。 事實(shí)上,Python 中不可能強(qiáng)制隱藏?cái)?shù)據(jù)——一切基于約定。 (如果需要,使用 C 編寫(xiě)的 Python 實(shí)現(xiàn)可以完全隱藏實(shí)現(xiàn)細(xì)節(jié)并控制對(duì)象的訪問(wèn)。這可以用來(lái)通過(guò) C 語(yǔ)言擴(kuò)展 Python。)

客戶應(yīng)該謹(jǐn)慎的使用數(shù)據(jù)屬性——客戶可能通過(guò)踐踏他們的數(shù)據(jù)屬性而使那些由方法維護(hù)的常量變得混亂。 注意:只要能避免沖突,客戶可以向一個(gè)實(shí)例對(duì)象添加他們自己的數(shù)據(jù)屬性,而不會(huì)影響方法的正確性——再次強(qiáng)調(diào),命名約定可以避免很多麻煩。

從方法內(nèi)部引用數(shù)據(jù)屬性(或其他方法)并沒(méi)有快捷方式。我覺(jué)得這實(shí)際上增加了方法的可讀性:當(dāng)瀏覽一個(gè)方法時(shí),在局部變量和實(shí)例變量之間不會(huì)出現(xiàn)令人費(fèi)解的情況。

一般,方法的第一個(gè)參數(shù)被命名為 self 。 這僅僅是一個(gè)約定:對(duì) Python 而言,名稱(chēng) self 絕對(duì)沒(méi)有任何特殊含義。(但是請(qǐng)注意:如果不遵循這個(gè)約定,對(duì)其他的 Python 程序員而言你的代碼可讀性就會(huì)變差,而且有些 類(lèi)查看器 程序也可能是遵循此約定編寫(xiě)的。) 類(lèi)屬性的任何函數(shù)對(duì)象都為那個(gè)類(lèi)的實(shí)例定義了一個(gè)方法。 函數(shù)定義代碼不一定非得定義在類(lèi)中:也可以將一個(gè)函數(shù)對(duì)象賦值給類(lèi)中的一個(gè)局部變量。 例如:

# 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?都是類(lèi)?C?的屬性,引用的都是函數(shù)對(duì)象,因此它們都是?C?實(shí)例的方法--?h?嚴(yán)格等于?g?。要注意的是這種習(xí)慣通常只會(huì)迷惑程序的讀者。

通過(guò)?self?參數(shù)的方法屬性,方法可以調(diào)用其它的方法:

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

方法可以像引用普通的函數(shù)那樣引用全局命名。與方法關(guān)聯(lián)的全局作用域是包含類(lèi)定義的模塊。(類(lèi)本身永遠(yuǎn)不會(huì)做為全局作用域使用。)盡管很少有好的理由在方法 中使用全局?jǐn)?shù)據(jù),全局作用域確有很多合法的用途:其一是方法可以調(diào)用導(dǎo)入全局作用域的函數(shù)和方法,也可以調(diào)用定義在其中的類(lèi)和函數(shù)。通常,包含此方法的類(lèi)也會(huì)定義在這個(gè)全局作用域,在下一節(jié)我們會(huì)了解為何一個(gè)方法要引用自己的類(lèi)。

每個(gè)值都是一個(gè)對(duì)象,因此每個(gè)值都有一個(gè) 類(lèi)(?class?) (也稱(chēng)為它的 類(lèi)型(?type?) ),它存儲(chǔ)為?object.__class__?。

繼承

當(dāng)然,如果一種語(yǔ)言不支持繼承就,“類(lèi)”就沒(méi)有什么意義。派生類(lèi)的定義如下所示:

class DerivedClassName(BaseClassName):  
    <statement-1>  
    .  
    .  
    .  
    <statement-N>  

命名?BaseClassName?(示例中的基類(lèi)名)必須與派生類(lèi)定義在一個(gè)作用域內(nèi)。除了類(lèi),還可以用表達(dá)式,基類(lèi)定義在另一個(gè)模塊中時(shí)這一點(diǎn)非常有用:

class DerivedClassName(modname.BaseClassName):

派生類(lèi)定義的執(zhí)行過(guò)程和基類(lèi)是一樣的。構(gòu)造派生類(lèi)對(duì)象時(shí),就記住了基類(lèi)。這在解析屬性引用的時(shí)候尤其有用:如果在類(lèi)中找不到請(qǐng)求調(diào)用的屬性,就搜索基類(lèi)。如果基類(lèi)是由別的類(lèi)派生而來(lái),這個(gè)規(guī)則會(huì)遞歸的應(yīng)用上去。

派生類(lèi)的實(shí)例化沒(méi)有什么特殊之處:?DerivedClassName()?(示列中的派生類(lèi))創(chuàng)建一個(gè)新的類(lèi)實(shí)例。方法引用按如下規(guī)則解析:搜索對(duì)應(yīng)的類(lèi)屬性,必要時(shí)沿基類(lèi)鏈逐級(jí)搜索,如果找到了函數(shù)對(duì)象這個(gè)方法引用就是合法的。

派生類(lèi)可能會(huì)覆蓋其基類(lèi)的方法。因?yàn)榉椒ㄕ{(diào)用同一個(gè)對(duì)象中的其它方法時(shí)沒(méi)有特權(quán),基類(lèi)的方法調(diào)用同一個(gè)基類(lèi)的方法時(shí),可能實(shí)際上最終調(diào)用了派生類(lèi)中的覆蓋方法。(對(duì)于 C++ 程序員來(lái)說(shuō),Python 中的所有方法本質(zhì)上都是?虛?方法。)

派生類(lèi)中的覆蓋方法可能是想要擴(kuò)充而不是簡(jiǎn)單的替代基類(lèi)中的重名方法。有一個(gè)簡(jiǎn)單的方法可以直接調(diào)用基類(lèi)方法,只要調(diào)用:BaseClassName.methodname(self,?arguments)。有時(shí)這對(duì)于客戶也很有用。(要注意只有?BaseClassName?在同一全局作用域定義或?qū)霑r(shí)才能這樣用。)

Python 有兩個(gè)用于繼承的函數(shù):

  • 函數(shù)?isinstance()?用于檢查實(shí)例類(lèi)型:?isinstance(obj,?int)?只有在obj.__class__?是?int?或其它從?int?繼承的類(lèi)型

  • 函數(shù)?issubclass()?用于檢查類(lèi)繼承:?issubclass(bool,?int)?為?True?,因?yàn)?bool?是 int 的子類(lèi)。但是,?issubclass(unicode,?str)?是?False?,因?yàn)?unicode?不是?str?的子類(lèi)(它們只是共享一個(gè)通用祖先類(lèi)basestring?)。

多繼承

Python 同樣有限的支持多繼承形式。多繼承的類(lèi)定義形如下例:

class DerivedClassName(Base1, Base2, Base3):  
    <statement-1>  
    .  
    .  
    .  
    <statement-N>  

在大多數(shù)情況下,在最簡(jiǎn)單的情況下,你能想到的搜索屬性從父類(lèi)繼承的深度優(yōu)先,左到右,而不是搜索兩次在同一個(gè)類(lèi)層次結(jié)構(gòu)中,其中有一個(gè)重疊。因此,如果在?DerivedClassName?(示例中的派生類(lèi))中沒(méi)有找到某個(gè)屬性,就會(huì)搜索?Base1?,然后(遞歸的)搜索其基類(lèi),如果最終沒(méi)有找到,就搜索?Base2?,以此類(lèi)推。

實(shí)際上,super()?可以動(dòng)態(tài)的改變解析順序。這個(gè)方式可見(jiàn)于其它的一些多繼承語(yǔ)言,類(lèi)似 call-next-method,比單繼承語(yǔ)言中的 super 更強(qiáng)大 。

動(dòng)態(tài)調(diào)整順序十分必要的,因?yàn)樗械亩嗬^承會(huì)有一到多個(gè)菱形關(guān)系(指有至少一個(gè)祖先類(lèi)可以從子類(lèi)經(jīng)由多個(gè)繼承路徑到達(dá))。例如,所有的 new-style 類(lèi)繼承自?object?,所以任意的多繼承總是會(huì)有多于一條繼承路徑到達(dá)?object?。

為了防止重復(fù)訪問(wèn)基類(lèi),通過(guò)動(dòng)態(tài)的線性化算法,每個(gè)類(lèi)都按從左到右的順序特別指定了順序,每個(gè)祖先類(lèi)只調(diào)用一次,這是單調(diào)的(意味著一個(gè)類(lèi)被繼承時(shí)不會(huì)影響它祖先的次序)??偹憧梢酝ㄟ^(guò)這種方式使得設(shè)計(jì)一個(gè)可靠并且可擴(kuò)展的多繼承類(lèi)成為可能。進(jìn)一步的內(nèi)容請(qǐng)參見(jiàn)http://www.python.org/download/releases/2.3/mro/?。

私有變量

只能從對(duì)像內(nèi)部訪問(wèn)的“私有”實(shí)例變量,在 Python 中不存在。然而,也有一個(gè)變通的訪問(wèn)用于大多數(shù) Python 代碼:以一個(gè)下劃線開(kāi)頭的命名(例如?_spam?)會(huì)被處理為 API 的非公開(kāi)部分(無(wú)論它是一個(gè)函數(shù)、方法或數(shù)據(jù)成員)。它會(huì)被視為一個(gè)實(shí)現(xiàn)細(xì)節(jié),無(wú)需公開(kāi)。

因?yàn)橛幸粋€(gè)正當(dāng)?shù)念?lèi)私有成員用途(即避免子類(lèi)里定義的命名與之沖突),Python 提供了對(duì)這種結(jié)構(gòu)的有限支持,稱(chēng)為?name mangling(命名編碼) 。任何形如?__spam?的標(biāo)識(shí)(前面至少兩個(gè)下劃線,后面至多一個(gè)),被替代為?_classname__spam?,去掉前導(dǎo)下劃線的?classname 即當(dāng)前的類(lèi)名。此語(yǔ)法不關(guān)注標(biāo)識(shí)的位置,只要求在類(lèi)定義內(nèi)。

名稱(chēng)重整是有助于子類(lèi)重寫(xiě)方法,而不會(huì)打破組內(nèi)的方法調(diào)用。 例如:

class Mapping:  
    def __init__(self, iterable)  
        self.items_list = []  
        self.__update(iterable)

    def update(self, iterable):  
        for item in iterable:  
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):  
        # provides new signature for update()  
        # but does not break __init__()  
        for item in zip(keys, values):  
            self.items_list.append(item)  

需要注意的是編碼規(guī)則設(shè)計(jì)為盡可能的避免沖突,被認(rèn)作為私有的變量仍然有可能被訪問(wèn)或修改。在特定的場(chǎng)合它也是有用的,比如調(diào)試的時(shí)候。 要注意的是代碼傳入?exec?,?eval()?或?execfile()?時(shí)不考慮所調(diào)用的類(lèi)的類(lèi)名,視其為當(dāng)前類(lèi),這類(lèi)似于?global?語(yǔ)句的效應(yīng),已經(jīng)按字節(jié)編譯的部分也有同樣的限制。這也同樣作用于?getattr()?,?setattr()?和 delattr()?,像直接引用?__dict__?一樣。

補(bǔ)充

有時(shí)類(lèi)似于 Pascal 中“記錄( record )”或 C 中“結(jié)構(gòu)( struct )”的數(shù)據(jù)類(lèi)型很有用,它將一組已命名的數(shù)據(jù)項(xiàng)綁定在一起。一個(gè)空的類(lèi)定義可以很好的實(shí)現(xiàn)它:

class Employee:  
pass 

john = Employee() # Create an empty employee record  
# Fill the fields of the record  
john.name = 'John Doe'    
john.dept = 'computer lab'   
john.salary = 1000    

某一段 Python 代碼需要一個(gè)特殊的抽象數(shù)據(jù)結(jié)構(gòu)的話,通常可以傳入一個(gè)類(lèi),事實(shí)上這模仿了該類(lèi)的方法。例如,如果你有一個(gè)用于從文件對(duì)象中格式化數(shù)據(jù)的函數(shù),你可以定義一個(gè)帶有?read()?和?readline()方法的類(lèi),以此從字符串緩沖讀取數(shù)據(jù),然后將該類(lèi)的對(duì)象作為參數(shù)傳入前述的函數(shù)。

實(shí)例方法對(duì)象也有屬性:m.im_self是一個(gè)實(shí)例方法所屬的對(duì)象,而 m.im_func?是這個(gè)方法對(duì)應(yīng)的函數(shù)對(duì)象。

異常也是類(lèi)

用戶自定義異常也可以是類(lèi)。利用這個(gè)機(jī)制可以創(chuàng)建可擴(kuò)展的異常體系。 以下是兩種新的,有效的(語(yǔ)義上的)異常拋出形式,使用?raise?語(yǔ)句:

raise Class 

raise Instance

第一種形式中,?instance?必須是?Class?或其派生類(lèi)的一個(gè)實(shí)例。第二種形式是以下形式的簡(jiǎn)寫(xiě):

raise Class()

發(fā)生的異常其類(lèi)型如果是?except?子句中列出的類(lèi),或者是其派生類(lèi),那么它們就是相符的(反過(guò)來(lái)說(shuō)--發(fā)生的異常其類(lèi)型如果是異常子句中列出的類(lèi)的基類(lèi),它們就不相符)。例如,以下代碼會(huì)按順序打印 B,C,D:

class B(Exception):  
    Pass  
class C(B):  
    Pass  
class D(C):  
     pass

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

要注意的是如果異常子句的順序顛倒過(guò)來(lái)(?execpt?B?在最前),它就會(huì)打印 B,B,B--第一個(gè)匹配的異常被觸發(fā)。

打印一個(gè)異常類(lèi)的錯(cuò)誤信息時(shí),先打印類(lèi)名,然后是一個(gè)空格、一個(gè)冒號(hào),然后是用內(nèi)置函數(shù)?str()?將類(lèi)轉(zhuǎn)換得到的完整字符串。

迭代器

現(xiàn)在你可能注意到大多數(shù)容器對(duì)象都可以用?for?遍歷:

for element in [1, 2, 3]:  
    print(element)  
for element in (1, 2, 3):  
    print(element)  
for key in {'one':1, 'two':2};  
    print(key)  
for char in "123":  
    print(char)  
For line in open("myfile.txt"):  
    print(line, end='')  

這種形式的訪問(wèn)清晰、簡(jiǎn)潔、方便。迭代器的用法在 Python 中普遍而且統(tǒng)一。在后臺(tái),?for?語(yǔ)句在容器對(duì)象中調(diào)用?iter()?。 該函數(shù)返回一個(gè)定義了?next()?方法的迭代器對(duì)象,它在容器中逐一訪問(wèn)元素。沒(méi)有后續(xù)的元素時(shí),?next()?拋出一個(gè)?StopIteration?異常通知?for?語(yǔ)句循環(huán)結(jié)束。以下是其工作原理的示例:

>>> s = 'abc'  
>>> it = iter(s)  
>>> it  
<iterator object at 0x00A1DB50>  
>>> next(it)  
'a'  
>>> next(it)  
'b'  
>>> next(it)  
'c'  
>>> next(it)  
Traceback (most recent call last):
  File "<stdin>", line 1, in ?  
    next(it)  
StopIteration  

了解了迭代器協(xié)議的后臺(tái)機(jī)制,就可以很容易的給自己的類(lèi)添加迭代器行為。定義一個(gè)?__iter__()?方法,使其返回一個(gè)帶有?next()?方法的對(duì)象。如果這個(gè)類(lèi)已經(jīng)定義了?next()?,那么?__iter__()?只需要返回?self:

class Reverse:  
    """Iterator for looping over a sequence backwards."""  
    def __init__(self, data):  
        self.data  = data  
        self.index = len(data)  
    def __iter__(self):  
        return self  
    def __next__(self):  
        if self.index == 0:  
            raise StopIteration  
        self.index = self.index - 1   
        return self.data[self.index] 
>>> rev = Reverse('spam')  
>>> iter(rev)  
<__main__.Reverse object at 0x00A1DB50>  
>>> for char in rev:  
...     print(char)  
...  
m 
a  
p  
s  

生成器

Generator?是創(chuàng)建迭代器的簡(jiǎn)單而強(qiáng)大的工具。它們寫(xiě)起來(lái)就像是正規(guī)的函數(shù),需要返回?cái)?shù)據(jù)的時(shí)候使用?yield?語(yǔ)句。每次?next()?被調(diào)用時(shí),生成器回復(fù)它脫離的位置(它記憶語(yǔ)句最后一次執(zhí)行的位置和所有的數(shù)據(jù)值)。以下示例演示了生成器可以很簡(jiǎn)單的創(chuàng)建出來(lái):

def reverse(data):  
    for index in range(len(data)-1, -1, -1): 
        yield data[index]  
>>> for char in reverse('golf'):  
...     print(char)  
...  
f  
l  
O  
g  

前一節(jié)中描述了基于類(lèi)的迭代器,它能作的每一件事生成器也能作到。因?yàn)樽詣?dòng)創(chuàng)建了__iter__()?和?next()?方法,生成器顯得如此簡(jiǎn)潔。

另一個(gè)關(guān)鍵的功能在于兩次執(zhí)行之間,局部變量和執(zhí)行狀態(tài)都自動(dòng)的保存下來(lái)。這使函數(shù)很容易寫(xiě),而且比使用?self.index?和?self.data?之類(lèi)的方式更清晰。

除了創(chuàng)建和保存程序狀態(tài)的自動(dòng)方法,當(dāng)發(fā)生器終結(jié)時(shí),還會(huì)自動(dòng)拋出 StopIteration?異常。綜上所述,這些功能使得編寫(xiě)一個(gè)正規(guī)函數(shù)成為創(chuàng)建迭代器的最簡(jiǎn)單方法。

生成器表達(dá)式

有時(shí)簡(jiǎn)單的生成器可以用簡(jiǎn)潔的方式調(diào)用,就像不帶中括號(hào)的鏈表推導(dǎo)式。這些表達(dá)式是為函數(shù)調(diào)用生成器而設(shè)計(jì)的。生成器表達(dá)式比完整的生成器定義更簡(jiǎn)潔,但是沒(méi)有那么多變,而且通常比等價(jià)的鏈表推導(dǎo)式更容易記。

例如:

>>> sum(i*i for i in range(10))              # sum of squares  
285  

>>> xvec = [10, 20, 30]    
>>> yvec = [7, 5, 3]    
>>> sum(x*y for x,y in zip(xvec, yvec))      # dot product  
260 

>>> from math import pi, sin  
>>> sine_table = {x: sin(x*pi/180) for x in range(0, 91)}

>>> unique_words = set(word for line in page  for word in   line.split())

>>>> valedictorian = max((student.gpa, student.name) for student in graduates) 

>>>> data = 'golf'  
>>> list(data[i] for i in range(len(data)-1, -1, -1))  
['f', 'l', 'o', 'g']  

腳注

[1] 有一個(gè)例外。模塊對(duì)象有一個(gè)隱秘的只讀對(duì)象,名為?__dict__?,它返回用于實(shí)現(xiàn)模塊命名空間的字典,命名?__dict__?是一個(gè)屬性而非全局命名。顯然,使用它違反了命名空間實(shí)現(xiàn)的抽象原則,應(yīng)該被嚴(yán)格限制于調(diào)試中。

上一篇:輸入和輸出下一篇:激起你的興趣