Python Tutorial(九):類

來源:互聯網
上載者:User
文章目錄
  • 9.2.1 範圍和命名空間樣本
  • 9.3.1 類定義文法
  • 9.3.2 類對象
  • 9.3.3 執行個體對象
  • 9.3.4 方法對象
  • 9.5.1 多重繼承

與其它程式設計語言相比,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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.