與其它編程語言相比,Python的類機制添加了最小的新語法和語義。它是C++和Modula-3中的類機制的混合。Python的類提供了面向對象編程的所有的標准特性,類繼承機制允許有多個基類,一個子類可以重寫基類中的任何方法,一個方法可以調用基類里面的同名方法。對象可以包含任意數量和種類的數據。就像模塊那樣,類參與Python的動態天性,在運行時被創建,創建后可以被進一步修改。
在C++術語中,通常類的成員(包括數據成員)是公有的,所有的成員函數都是虛的。在Modula-3中,從對象的方法里面引用對象的成員沒有簡寫形式。方法函數被聲明為擁有一個顯式的第一個參數來表示這個對象,在調用時隱式提供。就像在Smalltalk中,類它們本身就是對象。這提供引入和重命名的語義。不像C++和Modula-3,內建類型可以被用戶用作擴展的基類。像C++,許多內建的操作符都有特殊的語法,可以為類的實例重新定義。
9.1 名稱和對象簡介
同一個對象有個性的,和多個名稱(在多個范圍里)。在其它語言中稱為別名。通常第一次看Python時可以不用管它。在處理不可變的基本類型時可以被安全的忽略。當牽扯到可變對象時,別名對Python代碼的語義可能有一個驚奇的作用。這通常被用於程序的好處,因為別名的行為在某些方面像指針。例如,傳遞一個對象比較節省開銷,因為只有一個指針被傳遞。如果一個函數修改了作為參數被傳遞的對象,調用方將會看到這個改變,這樣就去除了兩種不同參數傳遞機制的需要,像在Pascal中那樣。
9.2 Python作用域和命名空間
在介紹類之前,我首先不得不告訴你一些有關Python作用域的規則。類定義對命名空間玩了一些整潔技巧,你需要知道作用域和命名空間如何工作,並且完全的理解正在發生什么。順便地,有關本主題的知識對於高級Python程序員是有用的。
讓我們以一些定義開始。
一個命名空間是一個從名稱到對象的映射。許多命名空間當前被實現為Python的字典,但是那通常以任何方式來說都不是顯而易見的,未來將會改變。命名空間的示例是:內建名稱的集合;模塊里的全局名稱;函數調用里的局部名稱。在某種意義上一個對象的屬性的集合也形成一個命名空間。關於命名空間需要知道的重要事情是不同的命名空間里的名稱之間絕對沒有關系。例如,兩個不同的模塊可以都定義一個函數maximize,不會產生混淆,模塊的用戶必須使用模塊名稱作為前綴。
順便提一下,任何跟在點后面的名稱我都用屬性稱呼它,例如,在表達式z.real中,real是對象z的一個屬性。嚴格的說,引用模塊里的名稱是屬性引用,在表達式modname.funcname,modname是一個模塊對象,funcname是它的一個屬性。在這種情況下,恰好有一個直接的映射在模塊的屬性和定義在模塊里的全局名稱,它們共享同樣的命名空間。
屬性可以是只讀的或可寫的。在后一種情況,可以對一個屬性賦值。模塊屬性是可寫的,你可以寫modname.the_answer = 42。可寫屬性也可以使用del語句進行刪除。例如,del modname.the_answer將移除屬性the_answer從名叫modname的對象。
命名空間在不同的時刻被創建,有不同的生命周期。包含內建名稱的命名空間在Python解釋器啟動的時候被創建,並且不再刪除。一個模塊的全局命名空間在模塊定義被讀入時創建,通常,模塊的命名空間也持續到解釋器的退出。通過解釋器的頂層調用執行的語句,要么從腳本文件讀入或交互式的,被認為是一個叫做__main__模塊的一部分,所以它們有它們自己的全局命名空間。(內建的名稱實際上也在一個模塊里,叫做builtins)
一個函數的局部命名空間在函數被調用時創建,在函數返回或引發一個在函數里沒有被處理的異常時被刪除。(實際上,忘記是一個較好的方式來描述實際發生了什么)當然,遞歸調用時,每一次調用都有它們自己的局部命名空間。
一個作用域是Python程序的一個正文區域,在那里一個命名空間被直接訪問。這里的直接訪問意味着一個對名稱的未限定引用將嘗試在命名空間里查找。
雖然作用域是靜態決定的,但是卻是動態使用的。在執行期間,至少有三個嵌套的作用域,它們的命名空間是直接被訪問:
- 最里層的作用域,首先被搜索,包含局部名稱。
- 任何的封閉函數作用域,從最近的封閉作用域開始搜索,包含非局部,但是也非全局的名稱。
- 倒數第二個作用域包含當前模塊的全局名稱。
- 最外層的作用域,最后搜索,包含內建名稱。
如果一個名稱被聲明為全局的,然后所有的引用和賦值直接到包含模塊全局名稱的中間作用域。要重新綁定在最內層作用域外發現的變量,nonlocal語句可以被使用,如果沒有聲明nonlocal,那些變量是只讀的(一個嘗試向這樣一個變量寫將簡單的在最里層作用域創建一個新的局部變量,同名的外層范圍變量沒有改變)。
通常,局部作用域引用當前函數的局部名稱。在函數外面,局部作用域引用和全局作用域相同的命名空間:模塊的命名空間。類定義位於局部作用域里的另一個命名空間。
重要的是要認識到作用域是本文的決定的。定義在模塊里的函數的全局作用域是那個模塊的命名空間,無論是從什么地方或通過什么別名來調用它。換句話說,實際對名稱的搜索是動態完成的,在運行時,然而,語言定義是朝着靜態名稱解決進化,在編譯時,所以不要依賴動態名稱解決(事實上,局部變量已經被靜態的決定)
Python的一個特別的巧合是,如果沒有global語句的影響,對一個名稱的賦值總是進入到最里層的作用域。賦值並不拷貝數據,它們僅僅把名稱綁定到對象。對於刪除這也是真的:語句del x從局部作用域引用的命名空間里刪除x的綁定。事實上,引入新名稱的所有操作使用局部作用域:特別的,import語句和函數定義在局部作用域里綁定模塊或函數名稱。
global語句用來指示特殊的變量駐留在全局作用域,應該在那里被彈回。nonlocal語句指示特殊的變量駐留在一個封閉的作用域,並且應該在那里被彈回。
9.2.1 作用域和命名空間示例
這個示例演示如何引用不同的作用域和命名空間,global和nonlocal如何影響變量的綁定:
示例代碼的輸出是:
注意,局部賦值是如何沒有改變spam的scope_test的綁定。nonlocal賦值改變了spam的scope_test的綁定,global賦值改變的是模塊級別的綁定。
你可以看到在global賦值之前沒有以前的對spam的綁定。
9.3 第一次看類
類引入了一小點新語法,三種新的對象類型,和一些新的語義。
9.3.1 類定義語法
最簡單的類定義形式像這樣:
類定義,像函數定義一樣,在它們有任何作用前必須先被執行。(你可以把類定義放到一個if語句的分支里,或一個函數里面)
在實踐中,一個類定義里面的語句通常是函數定義,其它語句也是允許的,並且有些時候比較有用。類里面的函數定義通常有一個特殊的參數列表形式。通過對方法的調用約定被支配。
當進入一個類定義時,一個新的命名空間被創建,並且用作局部作用域。因此,所有對局部變量的賦值都進入這個新的命名空間。特別的,函數定義把新函數的名稱綁定到這里。
當正常離開一個類定義時,一個類對象被創建。這是一個基本的包裝圍在通過類定義創建的命名空間的內容的外圍。下一節我們將學習更多有關類對象的內容。原始的局部作用域(進入類定義前,正在起作用的那個)被恢復,這個類的對象在這里綁定到類定義頭部給出的那個名稱上。
9.3.2 類對象
類對象支持兩種類型的操作,屬性引用和實例化。
在Python里,對於所有的屬性引用都使用標准的語法:obj.name。合法的屬性名稱是在類對象被創建時類的命名空間里的所有的名稱。所以,如果類的定義看起來像這樣:
那么MyClass.i和MyClass.f是合法的屬性引用,分別返回一個整數和一個函數對象。類屬性也可以被賦值,所以你可以通過賦值來改變MyClass.i的值。__doc__也是一個合法的屬性,返回類的文檔字符串。
類實例化使用函數寫法。假定類對象是一個沒有參數的函數,並且返回一個類的新實例。例如:
創建類的一個新實例並把這個對象賦給局部變量x。
實例化操作創建一個空的對象。許多類喜歡創建使實例定制到一個特定的初始狀態的對象。因此一個類可以定義一個特別的方法叫做__init__(),像這樣:
當類定義了一個__init__()方法時,對於一個新創建的類實例,類實例化會自動的調用__init__()方法。所以在這個示例里,一個新的、初始化的實例可以這樣獲得:
當然,__init__()方法可以有參數,這樣有更大的靈活性。在那種情況下,給類實例化操作符的參數被傳遞到__init__()方法。例如:
9.3.3 實例對象
那么現在我們可以對實例對象做些什么呢?惟一被實例對象理解的操作是屬性引用。有兩種合法的屬性名稱,數據屬性和方法。
數據屬性對應於Smalltalk中的實例變量,C++中的數據成員。數據屬性不需要被聲明,像局部變量一樣,當第一次被賦值時它們突然就存在了。例如,如果x是MyClass的實例,下面的代碼片段將打印出值16,沒有留下一個蹤跡:
另一種實例屬性引用是方法。方法是屬於對象的一個函數。(在Python里,術語方法對於類實例並不是惟一的,其它對象類型也有方法。)
一個實例對象合法的方法名取決於它的類。通過定義,一個類的所有是函數對象的屬性定義了它的實例的相應方法。所以在我們的示例里,x.f是一個合法的方法引用,因為MyClass.f是一個函數,但是x.i不是,因為MyClass.i不是。但是x.f和MyClass.f並不是一回事,它是一個方法對象,不是一個函數對象。
9.3.4 方法對象
通常一個方法會被立即調用在它被界定以后:
在MyClass示例里,這將會返回字符串'hello world'。然而,立即調用一個方法是沒有必要的。x.f是一個方法對象,可以被存儲到其它地方,並且在以后的時間調用。例如:
將一直打印hello world,直到最后。
當一個方法被調用時究竟發生了什么?你應該注意到x.f()被調用而沒有參數,即使f()的函數定義指定了一個參數。那么參數發生了什么呢?可以確定的是Python會引發一個異常當一個函數要求一個參數但在調用時沒有,即使這個參數實際上並不使用。
事實上,你或許已經猜到了答案,關於方法的特別的事情是對象作為函數的第一個參數被傳入。在我們的示例中,調用x.f()和MyClass.f(x)是相等的。一般來說,調用一個帶有n個參數的方法等同於調用相應的函數,並把方法所屬的對象插入到參數列表的第一個參數前面。
如果你仍然不理解方法如何工作,看一下實現也許會使事情變得清晰。當一個不是數據屬性的實例屬性被引用時,它的類被尋找。如果這個名稱指示一個合法的類屬性是一個函數對象,一個方法對象通過把這個實例對象和那個一起發現的函數對象打包進一個抽象對象的方式被創建,這就是這個方法對象。當這個方法對象使用一個參數列表被調用時,一個新的參數列表將從這個實例對象和這個參數列表被構造,這個函數對象將使用這個新的參數列表被調用。
9.4 隨機備注
數據屬性重寫了相同名稱的方法屬性;為了避免意外的名稱沖突,這將會引起比較難發現的問題在大程序里,一個聰明的做法是使用一些約定來把沖突的機會降到最小。可能的約定包括方法名稱大寫,數據屬性名稱加一個小的惟一字符串前綴(或許就是一個下划線),或方法使用動詞,數據屬性使用名詞。
數據屬性可以被方法和一個對象的普通用戶引用。換句話說,類不是可用的對於實現純抽象數據類型。事實上,在Python里面沒什么東西能夠強迫數據隱藏,它都是基於約定的。(從另一方面說,Python是用C實現的,可以完全的隱藏實現細節和控制對一個對象的訪問,如果有必要的話;這一點可以通過C寫的Python擴展來使用。)
客戶端應該細心的使用數據屬性,客戶端或許攪亂不變的維護通過方法沖壓它們的數據屬性。注意,客戶端可以添加它們自己的數據屬性到一個實例對象上而不影響方法的合法性,和名稱沖突被避免,一個命名約定在這里可以省去許多頭疼的事情。
在一個方法里面沒有簡單的寫法來引用數據屬性(或其它方法)。我發現實際上這增加了方法的可讀性,在翻閱方法時,不存在局部變量和實例變量沖突的機會。
通常,方法的第一個參數叫做self。這就是一個約定,self這個名字對於Python絕對沒有特別的意義。注意,如果你的代碼不跟隨約定的話對於其它Python程序員來說可讀性會降低。也可以想象,一個類瀏覽程序也需要依賴於這個約定來寫。
任何函數對象是一個類屬性,定義了這個類的實例的一個方法。函數定義被本文的封閉在類定義里面不是必須的。把一個函數對象賦給類的局部變量也是可以的。例如:
現在f,g和h都是類C的屬性並且指向函數對象,所以它們都是C的實例的方法,h和g是相等的,這樣的實踐通常會是程序的讀者困惑。
方法可以調用其它方法通過使用self參數的方法屬性:
方法可以引用全局名稱以像普通函數那樣的方式。和一個方法關聯的全局作用域是包含它定義的模塊。(一個類從來不被用作一個全局作用域)一個人比較罕見的遇到了在一個方法里面使用全局數據的好理由,有許多全局作用域的合法使用。首先,導入到全局作用域里面的函數和模塊可以被方法和定義在它里面的函數和類使用。通常,包含方法的類它自己定義在這個全局作用域里面,在下一節我們將發現一些好的理由,為什么一個方法想要引用它自己的類。
每一個值都是一個對象,因此有一個類(也叫做它的類型)它被存儲為objct.__class__。
9.5 繼承
當然,一個語言特性將不值得擁有名稱類,如果它不支持繼承。一個子類定義的語法看起來像這樣:
名稱BaseClassName必須定義在包含子類定義的作用域里。在基類名稱的那個地方,其它的任意表達式也是允許的。這會比較有用,例如,當基類定義在其它模塊里面:
子類定義的執行的進行和基類是一樣的。當類對象被構建時,基類被記住。這用來解析屬性引用,如果一個要求的屬性在類里面沒有發現,搜索將進行到基類里面。這個規則將遞歸的往下應用如果這個基類也繼承了其它的類。
關於子類的實例化並沒有什么特別之處,DerivedClassName()創建一個新的類實例。方法引用按下面方式被解析,相應的類屬性被搜索,如果必要的話沿着基類的鏈往下進行,如果這能返回一個函數對象,方法引用就是合法的。
子類可以重寫基類的方法。因為方法沒有特權當調用同一個對象的其它方法時,一個基類的方法調用同一個基類的另一個方法將結束調用一個重寫它的子類的一個方法。(對於C++程序員,所有Python里面的方法實際上都是虛的)
一個子類里面的重寫方法事實上是想擴展而不是簡單的替換基類里面的同名方法。有一個簡單的方式可以直接調用基類里面的方法,就調用BaseClassName.methodname(self, arguments)。這偶爾對客戶也非常有用。(注意,在這個全局作用域里面,如果基類作為BaseClassName是可訪問的,上面的方法才可以工作)
Python有兩個內建函數和繼承一起使用:
- 使用isinstance()來檢測一個實例的類型,isinstance(obj, int)將返回True當且僅當obj.__class__是int或某個類繼承自int。
- 使用issubclass()來檢測類繼承,issubclass(bool, int)是True,因為bool是int的一個子類。然而,issubclass(float, int)是False,因為float不是int的一個子類。
9.5.1 多重繼承
Python也支持多重繼承的形式。一個帶有多個基類的類定義看起來像這樣:
基於多種目的,在最簡單的情況下,你可以認為搜索一個從父類繼承過來的屬性是深度優先,從左到右,不會在一個類里面搜索兩次當它處於繼承層次的重疊處時。因此,如果一個屬性在DerivedClassName里面沒有發現,然后搜索Base1,然后遞歸的搜索Base1的所有基類,如果在那里沒有發現,然后搜索Base2,等等。
事實上,比上面稍微復雜些,方法的解析順序動態的改變來支持協作的調用super()。這種方式也出現在其它一些多繼承語言中,比單繼承里面的super調用更強大。
動態排序是必要的,因為多重繼承的所有情況都展示出一個或多個菱形關系(那里至少有一個父類可以在最底層的類里面通過多個路徑訪問到。)例如,所有的類都繼承自object,所以多繼承的任何情況都提供多於一條的路徑到達object。為了保持基類被訪問不多於一次,動態算法以一個方式線性化搜索順序,保持在每一個類里面指定的從左到右的順序,那就調用每一個父類僅一次,那就是單調的(意味着一個類可以被子類化而不影響它父類的優先權順序)。綜合起來,這些屬性使采用多重繼承來設計可靠的和可擴展的類變成可能。
9.6 私有變量
除了在一個對象里面否則不能被訪問的私有實例變量在Python里是不存在的。然而,有一個被多數Python代碼遵循的約定,一個以下划線為前綴的名稱應該被認為是API的非公共部分(它是否是一個函數,一個方法或一個數據成員)。它應該被認為是一個詳細實現,並且服從改變而不用通知。
因為有一個合法的類私有成員用例(即避免子類定義的名稱造成的命名沖突),對於這個機制有一個有限的支持,叫做名稱矯正。任何__spam這個形式的標識符(至少兩個前導下划線,最多一個尾部下划線)被本文的替換為_classname__spam,這里的classname是當前的類的名稱,並去掉前導的下划線。這個矯正被完成和標識符的語法位置無關,只要它發生在一個類的定義內部。
名稱矯正是有用的,它讓子類重寫方法而不打破類內部方法調用。例如:
注意,矯正規則被設計大多數是為了避免事故,它也可以用來訪問或修改一個被認為是私有的變量。在特別的情況下這個甚至有用,就像調試。
注意,傳遞給exec()或eval()的代碼不認為正在調用的類的名稱是當前的類,這和global語句的作用是相似的,它的作用對於字節編譯在一起的代碼是同樣的限制的。同樣的限制應用於getattr(),setattr()和delattr(),和當直接引用__dict__時。
9.7 零碎的
有時,有一個像Pascal的記錄或C的結構是非常有用的,把少數的命名數據項打包在一起。一個空的類定義就可以很好的完成:
一段Python代碼希望一個特別的抽象數據類型,經常被傳入一個類來模擬那個數據類型的方法所取代。例如,如果你有一個函數格式化一些來自文件對象的數據,你可以定義一個類,有read()和readline()方法從一個字符串緩沖區獲得數據,並且作為參數傳遞個它。
實例方法對象也有屬性,m.__self__是擁有方法m()的實例對象,m.__func__是和方法對應的函數對象。
9.8 異常也是類
用戶定義的異常也是通過類來標識的。使用這個機制可以創建異常的可擴展層次。
有兩個新的合法的語義形式對於raise語句:
第一種形式,Class必須是type或它的子類的實例。它是下面的一個簡寫:
一個except從句中的類是和一個異常可匹配的,如果它和異常是同一個類或是異常的一個基類(其它方式不行,一個except從句列出一個子類,和一個基類是不匹配的)。例如,下面的代碼將按照那樣的順序打印B,C,D:
如果except從句反轉,把except B放到第一,將會打印出BBB,第一個匹配except從句被觸發。
當一個未處理的異常的錯誤信息被打印出來時,異常的類名稱被打印,然后一個冒號和一個空格,最后是實例的字符串表示形式,使用內建的str()函數進行轉化。
9.9 迭代器
到現在你可能已經注意到許多容器對象可以使用for語句在它上面進行迭代:
訪問的樣式清晰,簡潔,方便。迭代器的使用遍及和統一Python。在這個場景后面,for語句在容器對象上調用iter()。函數返回一個迭代器對象,它定義了方法__next__(),它每次訪問一個容器中的元素。當沒有更多元素時,它就引發一個StopIteration異常告訴for循環來終止。你可以使用內建的next()函數調用__next__()方法。下面的示例演示它如何工作:
已經看到了迭代器協議背后的結構,可以很容易的給你的類加上迭代器行為。定義一個__iter__()方法,並返回一個包含__next__()方法對象。如果類定義__next__(),然后__iter__()可以僅僅返回它自己:
9.10 生成器
生成器是一個簡單和強大的工具來創建迭代器。它們寫起來就像正常的函數,但是任何時候它們想返回數據的時候使用yield語句。每一次在它上面調用next()時,生成器在它離開的地方重新開始(它能記住所有的數據值和最后一次執行的語句)。一個示例演示生成器可以很簡單的被創建:
任何能夠用生成器完成的事情也能夠用基於迭代器的類來完成。使生成器如此兼容的是__iter__()和__next__()方法被自動創建。
另一個關鍵的特征是局部變量和執行狀態在每次調用之間被自動的保存。這使得函數更容易書寫和比使用像self.index和self.data這樣的實例變量的方式更加清晰。
除了自動方法創建和保存程序狀態,當生成器終止時,它們自動的引發StopIteration異常。總之,這些特性使創建一個迭代器很容易,並不比寫一個正常的函數多付出努力。
9.11 生成器表達式
一些簡單的生成器可以被簡潔的寫為表達式,使用一個和列表綜合相似的語法,但是要用小括號代替大括號。這些表達式被設計為在那里生成器被一個封閉的函數立馬使用的情況。生成器表達式比完整的生成器定義更加緊湊但功能較少和比相等的列表綜合趨向於更友好的內存使用。
示例:
本文是對官方網站內容的翻譯,原文地址:http://docs.python.org/3/tutorial/classes.html