本節書摘來自華章出版社《資料結構與算法:python語言描述》一書中的第2章,第2.3節,作者 裘宗燕,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視
前面給出了兩個有理數類的定義,幫助讀者得到一些有關python類機制的直覺認識。本節将介紹python類定義的進一步情況。本書中對類的使用比較規範,涉及的與python類定義相關的機制不多,隻需要有最基本的了解就可以學習後面内容。另一方面,本書的主題是資料結構和算法,并不計劃全面完整地介紹python語言的面向對象機制和各種使用技術。本節主要想給讀者提供一些可參考的基本材料,是以,下面有關python語言的相關介紹将限制在必要的範圍内,供讀者參考,不深入讨論。有關python面向對象技術的更多細節,請讀者參考其他書籍和材料。
本小節介紹python類定義和使用的基本情況。
類定義
類定義的基本文法是:
一個類定義由關鍵詞class開始,随後是使用者給定的類名,一個冒号,以及一個語句組。這個語句組稱為類(定義)的體部分。
與函數定義類似,類定義也是python的一種語句。類定義的執行效果(語義)就是建立起這個定義描述的類。在python裡建立的類也是一種對象,表示一個資料類型。類對象的主要作用就是可以建立這個類的執行個體(稱為該類的執行個體對象)。
一個類定義确定了一個名字空間,位于類體裡面的定義都局部于這個類體,這些局部名字在該類之外不能直接看到,不會與外面的名字沖突。在執行一個類定義時,将以該定義為作用域建立一個新的名字空間。類定義裡的所有語句(包括方法定義)都在這個局部名字空間裡産生效果。這樣建立的類名字空間将一直存在,除非明确删除(用del)。當一個類定義的執行完成時,python解釋器建立相應的類對象,其中包裝了該類裡的所有定義,然後轉回到原來的(也就是該類定義所在的)名字空間,在其中建立這個新的類對象與類名字的限制。在此之後通過類名字就能引用相應的類對象了。
在很多類定義的體裡隻有一組def語句(前面rational類就是如此),為這個類定義一組局部函數。實際上,完全可以在這裡寫其他語句,其可能用途後面有簡單讨論。類裡定義的變量和函數等稱為這個類的屬性。這裡的函數定義常采用一種特殊形式,下面将詳細介紹具有這種特殊形式的函數與“方法”之間的關系。
類定義可以寫在程式裡的任何地方,這一點與函數定義的情況類似。例如,可以把類定義放在某個函數定義裡,或者放在另一類定義裡,效果是在那裡建立一個局部的類定義。但在實踐中,人們通常總是把類定義寫在子產品最外層,這樣定義的(類)類型在整個子產品裡都可以用,而且允許其他子產品裡通過import語句導入和使用。
類對象及其使用
前面說了,執行一個類定義将建立起一個類對象,這種對象主要支援兩種操作:屬性通路和執行個體化(即建立這個類的執行個體對象)。
在python語言裡,所有屬性引用(屬性通路)都采用圓點記法。例如,基于子產品名引用其中的函數(如math.sin(...)等),類也是如此。在程式裡,可以從類名出發,通過屬性引用的方式通路有定義的屬性,取得它們的值。類裡的資料屬性(相當于在類裡有定義的局部變量)可以儲存各種值,類中函數定義生成其函數屬性,這種函數屬性的值就是一個函數對象,可以通過類名和屬性名,采用圓點記法調用。此外,每個類對象都有一個預設存在的__doc__資料屬性,其值是該類的文檔串。
在定義好一個類後,可以通過執行個體化操作建立該類的執行個體對象。執行個體化采用函數調用的文法形式,最簡單情況就像是調用一個無參函數,例如
假設classname是一個已有定義的類。上面語句将建立classname類的一個新執行個體(執行個體對象),并把該對象賦給變量x。如果classname類裡沒有定義初始化函數,這一簡單調用建立的是該類的一個空對象,其中沒有資料屬性。
雖然python允許先建立空對象,而後再逐漸加入所需的屬性,但在實際程式設計中人們大都不這樣做。這樣做不太合适的主要原因有兩個:一是因為一個個地給對象加入屬性,工作很瑣碎也很麻煩;更重要的是這樣建立出屬于同一個類的對象,需要自己維持某種規範性,否則就不能保證在程式運作中的安全使用。
執行個體對象的初始化
建立一個類的執行個體對象時,人們通常希望對它做一些屬性設定,保證建立起的對象狀态完好,具有所需要的性質,也就是說,希望在建立類的執行個體對象時自動完成适當的初始化。python類中具有特殊名字__init__的方法自動完成初始化工作:
如果在一個類裡定義了__init__方法,在建立這個類的執行個體時,python解釋器就會自動調用這個方法。
__init__方法的第一個參數(通常用self作為參數名)總表示目前正在建立的對象。方法體中可以通過屬性指派的方式(形式為self.fname的指派,fname是自己標明的屬性名)為該對象定義屬性并設定初始值。
__init__可以有更多形式參數。如果存在這種參數,在建立該類的執行個體對象時,就需要為(除第一個self之外的)形式參數提供實際參數值,用表達式寫在類名後面的實參表裡。在建立執行個體對象時,這些實際參數将被送給__init__,使它可以基于這些實參對執行個體對象做特定的初始化。
在前面定義有理數類時定義了初始化函數。是以,語句
twothirds = rational(2, 3)
的執行中将完成一系列動作:①建立一個rational類型的對象;②調用rational類的__init__函數給這個對象的兩個屬性指派;③傳回建立的對象。最後,指派語句把這個新對象賦給變量twothirds,作為其限制值。
類執行個體(對象)的資料屬性
對于已有的類執行個體對象,可以通過屬性引用的方式通路其資料屬性。在python語言裡,類執行個體的資料屬性不需要專門聲明,隻要給對象的屬性指派,就會自動建立這種屬性(就像普通變量一樣)。每個執行個體對象是一個局部名字空間,其中包含該對象的所有資料屬性及其限制值,對象的全體資料屬性的取值情況構成該對象的狀态。如果建立的是空對象,它就有一個空的名字空間。如果在類裡定義了初始化函數,建立的執行個體對象就會包含該函數設定的屬性。例如,上面語句建立的rational類的執行個體有一個局部名字空間,其中包含兩個屬性名num和den,它們被分别限制于整數值2和3。
由于上述情況,人們在定義類時,通常總是通過自動調用的__init__函數建立執行個體對象的初始狀态,用類裡定義的其他函數檢視或修改執行個體對象的狀态。實際上,python允許在任意方法裡給原本沒有的(也就是說,初始化函數沒建立的)屬性指派,這種指派将擴大該對象的名字空間。但在實際中這種做法并不多見。
一個執行個體對象是一個獨立的資料體,可以像其他對象一樣賦給變量作為限制值,或者傳進函數處理,或者作為函數的結果傳回等。執行個體對象也可以作為其他執行個體對象的屬性值(無論是同屬一個類的執行個體對象,或不屬于同一個類的執行個體對象),這種情況形成了更複雜的對象結構。在複雜的程式裡,這種情況很常見。
方法的定義和使用
除資料屬性外,類執行個體的另一類屬性就是方法。
在一個類定義裡按預設方式定義的函數,都可以作為這個類的執行個體對象的方法。但是,如果确實希望類裡的一個函數能作為該類執行個體的方法使用,這個函數至少需要有一個表示其調用對象的形參,放在函數定義的參數表裡的第一個位置。這個形參通常取名self(實際上可以用任何名字,用self作為參數名是python社團的習慣做法)。除了self之外,還可以根據需要為函數引入更多形參。下面将稱類裡的這種函數為(執行個體)方法函數。除了是在類裡定義而且至少有一個形參外,方法函數并沒有别的特殊之處,從其他方面看它們就是一般的函數,python關于函數的規定在這裡都适用。
簡單地說,如果類裡定義了一個方法函數,這個類的執行個體對象就可以通過屬性引用的方式調用這個函數。在用x.method(…) 的形式調用方法函數method時,對象x将被作為method的第一個實參,限制到方法函數的第一個形參self,其他實參按python有關函數調用的規定分别限制到method的其他形參,然後執行該函數的體代碼。
說得更準确些。如果在程式裡通過某個類c的執行個體對象o,以屬性引用的形式調用類c裡定義的方法函數m,python解釋器就會建立一個方法對象,把執行個體對象o和方法函數m限制在這個方法對象裡。在(後面)執行這個方法對象時,o就會被作為函數m的第一個實參。在函數m的定義裡通過形參self的屬性通路,都實作為對調用對象o的屬性通路(取值或指派)。看一個具體例子(和寫法):
假設在類c裡定義了方法函數m,c.m就是一個函數,其值是普通的函數對象,就像采用其他方式定義的函數一樣,例如math.sin的值就是一個函數對象。
假設變量p的值是類c的一個執行個體,表達式p.m的值就是基于這個執行個體和函數m建立的一個方法對象。
使用方法對象的最常見方式是直接通過類執行個體做方法調用。例如,假設類c的方法函數m有三個形參,變量p的值是類c的執行個體,從p出發調用m就應寫成p.m(a, b) 的形式,這裡假設a和b是适合作為另兩個參數的表達式。
從上面的說明不難看到,方法調用p.m(…) 實際上等價于函數調用c.m(p, …)。方法的其他參數可以通過調用表達式中的其他實參提供。
方法對象也是一種(類似函數的)對象,可以作為對象使用。例如,可以把方法對象賦給變量,或者作為實參傳入函數,然後在函數的其他地方作為函數去調用。在上面假設的情況下,程式裡完全可以寫“q=p.m”,而後可以在其他地方寫調用q(a, b),表示用a和b作為實參調用這個方法。
注意,方法對象和函數對象不同,它實際上包含了兩個成分:一個是由類中的函數定義生成的函數對象,另一個是調用時限制的(屬于相應類的)一個執行個體對象。在這個方法對象最終執行時,其中的執行個體對象将被作為函數的第一個實參。
對于類定義、方法定義等機制,有下面幾點說明:
在執行了一個類定義,進而建立了相應的類對象之後,還可以通過屬性指派的方式為這個類(對象)增加新屬性。不僅可以為其增加資料屬性,也可以增加函數屬性。但是這時需要特别當心,如果新屬性與已有函數屬性同名,就會覆寫同名的屬性,這種情況有時可能是程式設計錯誤。人們一般采用特殊的命名規則避免這種錯誤。同樣情況也可能出現在__init__方法裡。在初始化方法裡指派的屬性與類定義中的方法同名是一種常見程式設計錯誤,應特别注意。舉例說,假設rational類定義了名字為num的解析操作,如果在__init__函數裡給self.num指派,就會覆寫同名的方法定義。前面類定義裡的資料屬性名是 _num,也避免了這種名字沖突。
如果需要在一個方法函數裡調用同一個類裡的其他方法函數,就需要明确地通過函數的第一個參數(self),以屬性描述的方式寫方法調用。例如,在方法函數f裡調用另一個方法函數g,應該寫self.g(...)。
從其他方面看,方法函數也就是定義在類裡面的函數。其中也可以通路全局名字空間裡的變量和函數,必要時也可以寫global或nonlocal聲明。
python提供了一個内置函數isinstance,專門用于檢查類和對象的關系。表達式isinstance(obj, cls)檢查對象obj是否為類cls的執行個體,當obj的類是cls時得到true,否則得到false。實際上,isinstance可以用于檢測任何對象與任何類型的關系。例如檢查一個變量或參數的值是否為int類型或float類型等。
靜态方法和類方法
除了前面介紹的執行個體方法之外,類裡還可以定義另外兩類函數:
第一類是前面介紹過的靜态方法,定義形式是在def行前加修飾符 @staticmethod。靜态方法實際上就是普通函數,隻是由于某種原因需要定義在類裡面。靜态方法的參數可以根據需要定義,不需要特殊的self參數。可以通過類名或者值為執行個體對象的變量,以屬性引用的方式調用靜态方法。例如,在前面rational類裡用rational._gcd(...)的形式調用靜态方法_gcd,也可以寫self._gcd(...)。注意,靜态方法沒有self參數。這也意味着,無論采用上面哪種調用形式,參數表裡都必須為每個形參提供實參,這裡沒有自動使用的self參數。
類裡定義的另一類方法稱為類方法,定義形式是在def行前加修飾符 @classmethod。這種方法必須有一個表示其調用類的參數,習慣用cls作為參數名,還可以有任意多個其他參數。類方法也是類對象的屬性,可以以屬性通路的形式調用。在類方法執行時,調用它的類将自動限制到方法的cls參數,可以通過這個參數通路該類的其他屬性。人們通常用類方法實作與本類的所有對象有關的操作。
這裡舉一個例子。假設所定義的類需要維護一個計數器,記錄程式運作中建立的該類的執行個體對象的個數。可以采用下面的定義:
類定義的其他部分省略。
為了記錄本類建立的對象個數,countable類裡定義了一個資料屬性counter,其初值設定為0。每次建立這個類的對象時,初始化方法__init__就會把這個對象計數器加一。類方法get_count通路了這個資料屬性。上面程式片段在運作時将輸出整數3,表示到執行print語句為止已經建立了3個countable對象。
類定義的作用域規則
類定義作為python語言裡的一種重要定義結構,也是一種作用域機關。在類裡定義的名字(辨別符)具有局部作用域,隻在這個類裡可用。如果需要在類定義之外使用,就采用基于類名字的屬性引用方式。例如,下面定義是合法的:
然而,在前面例子裡可以看到一個情況:counter是類countable的資料屬性,但是在countable類的兩個方法裡,都是通過類名和圓點形式,采用屬性引用的形式通路counter。實際上,在python裡必須這樣做,在這方面,類作用域裡的局部名字與函數作用域裡局部名字有不同的規定。
對于函數定義,其中局部名字的作用域自動延伸到内部嵌套的作用域。正因為這樣,如果在一個函數f裡定義局部函數g,在g的函數體裡可以直接使用f裡有定義的變量,或使用在f裡定義其他局部函數,除非這個名字在g裡另有定義。
對于類定義,情況則不是這樣。在類c裡定義的名字(c的資料屬性或函數屬性名),其作用域并不自動延伸到c内部嵌套的作用域。是以,如果需要在類中的函數定義裡引用這個類的屬性,一定要采用基于類名的屬性引用方式。
私有變量
在面向對象的程式設計領域,人們通常把類執行個體對象裡的資料屬性稱作執行個體變量。因為它們就像是定義在執行個體對象的名字空間裡的變量。
在一些面向對象語言裡,允許把一些執行個體變量定義為私有變量,隻允許在類定義的内部通路它們(也就是說,隻允許在執行個體對象的方法函數裡通路),不允許在類定義之外使用。實際上,在類之外根本就看不到這種變量,這是一種資訊隐藏機制。python語言裡沒有為定義私有變量提供專門機制,沒有辦法說明某個屬性隻能在類的内部通路,隻能通過程式設計約定和良好的程式設計習慣來保護執行個體對象裡的資料屬性。
在python程式設計實踐中,習慣約定是把以一個下劃線開頭的名字作為執行個體對象内部的東西,永遠不從對象的外部去通路它們。無論這樣的名字指稱的是(類或類執行個體的)資料成員、方法,還是類裡定義的其他函數。也就是說,在程式設計中永遠把具有這種名字的屬性看作類的實作細節。在前面的rational類裡,資料屬性_num和_den、函數屬性_gcd都是這種情況,在這個類定義之外都不應該使用。
另外,如果一個屬性以兩個下劃線開頭(但不是以兩個下劃線結尾),在類之外采用屬性通路方式直接寫這個名字将無法找到它。python解釋器會對類定義具有這種形式的名字做統一的改名。有關情況見python語言文檔。
此外,具有__add__形式(前後各有兩個下劃線)的名字有特殊的意義,除了前面介紹過的表示各種算術運算符、比較運算符的特殊名字和__init__、__str__之外,還有一大批特殊名字。有關細節請檢視python語言文檔。
實際上,在python程式設計中,上述約定不僅僅針對類及其執行個體對象,也适用于子產品等一切具有内部結構的對象。
基于類和對象的程式設計被稱為面向對象的程式設計,在這裡的基本工作包括三個方面:定義程式裡需要的類(也是定義新類型);建立這些類的(執行個體)對象;調用對象的方法完成計算工作,包括完成對象之間的資訊交換等。
在python語言裡做面向對象的程式設計,首先要根據程式的需求定義一組必要的類。前面幾小節已經介紹了類定義的基本機制,本節将介紹另一種重要機制—繼承。繼承的作用主要有兩個:一個是可以基于已有的類定義新類,通過繼承的方式複用已有類的功能,重複利用已有的代碼(已有的類定義),減少定義新類的工作量,簡化新功能的開發,提高工作效率。另一個作用實際上更重要,就是建立一組類(類型)之間的繼承關系,利用這種關系有可能更好地組織和構造複雜的程式。
繼承、基類和派生類
在定義一個新的類時,可以列出一個或幾個已有的類作為被繼承的類,這樣就建立了這個新定義類與指定的已有類之間的繼承關系。通過繼承定義出的新類稱為所列已有類的派生類(或稱子類),被繼承的已有類則稱為這個派生類的基類(或父類)。派生類将繼承基類的所有功能,可以原封不動地使用基類中已定義的功能,也可以根據需要修改其中的一些功能(也就是說,重新定義其基類已有的某些函數屬性)。另一方面,派生類可以根據需要擴充新功能(定義新的資料和/或函數屬性)。
在概念上,人們把派生類(子類)看作其基類(父類)的特殊情況,它們的執行個體對象集合具有一種包含關系。假設類c是類b的派生類,c類的對象也看作c的基類b的對象。人們經常希望在要求一個類b的執行個體對象的上下文中可以使用其派生類c的執行個體對象。這是面向對象程式設計中最重要的一條規則,稱為替換原理。許多重要的面向對象程式設計技術都需要利用類之間的繼承關系,也就是利用替換原理。
一個類可能是其他類的派生類,它又可能被用作基類去定義新的派生類。這樣,在一個程式裡,所有的類根據繼承關系形成了一種層次結構(顯然不能出現類之間的循環繼承,這種情況是程式設計錯誤,python系統很容易檢查這種錯誤)。python有一個最基本的内置類object,其中定義了一些所有的類都需要的功能。如果一個類定義沒說明基類,該類就自動以object作為基類。也就是說,任何使用者定義類都是object的直接或間接派生類。另外,python系統定義了一批内置類,各種基本類型形成了一套層次結構。系統中的内置異常也形成了一套層次結構,有關情況在2.4節簡單介紹。
基于已有類baseclass定義派生類的文法形式是:
列在類名後面括号裡的“參數”就是指定的基類,可以有一個或者多個,它們都必須在這個派生類定義所在的名字空間裡有定義。python允許用更複雜的表達式描述所需要的基類,隻要這個表達式的值确實是個類對象。例如,可以在用import語句導入另一個子產品之後,利用在該子產品裡有定義的類作為基類,定義自己的派生類。
python内置函數issubclass檢查兩個類是否具有繼承關系,包括直接的或間接的繼承關系。如果cls2是cls1直接的或間接的基類,表達式issubclass(cls1,cls2)将傳回true,否則傳回false。實際上,python的一些基本類型之間也有子類(子類型)關系。詳情請檢視标準庫手冊中有關基本類型的介紹。
作為最簡單的例子,下面定義了一個自己的字元串類:
這個類将繼承内置類型str的所有功能,沒做任何修改或擴充。但它是另一個新類,是str的一個派生類。有了這個定義,就可以寫:
第一個語句建立了一個mystr類型的對象,後三個表達式的值都是true,其中issubclass判斷子類關系(派生關系),顯然mystr是str的派生類。最後一個表達式為真,是因為派生類的對象也是基類的對象。
派生類常需要重新定義__init__函數,完成該類執行個體的初始化。常見情況是要求派生類的對象可以作為基類的對象,用在要求基類對象的環境中。在使用這種對象時,可能調用派生類自己定義的方法,也可能調用由基類繼承的方法。是以,在這種派生類的執行個體對象裡就應該包含基類執行個體的所有資料屬性,在建立派生類的對象時,就需要對基類對象的所有資料屬性進行初始化。完成這一工作的常見方式是直接調用基類的__init__方法,利用它為正建立的執行個體裡那些在基類執行個體中也有的資料屬性設定初值。也就是說,派生類__init__方法定義的常見形式是:
這裡繼承baseclass類定義派生的derivedclass類。在調用基類的初始化方法時,必須明确寫出基類的名字,不能從self出發去調用。在調用基類的__init__時,必須把表示本對象的self作為調用的第一個實參,可能還需要傳另一些(合适的)實參。這個調用完成派生類執行個體中屬于基類的那部分屬性的初始化工作。
在派生類裡覆寫基類中已定義的函數時,也經常希望新函數是基類同名函數的某種擴充,也就是說,希望新函數包含被覆寫函數的已有功能。這種情況與__init__的情況類似,處理方法也類似:在新函數定義裡,可以用baseclass.methodname(...) 的形式調用基類方法。實際上,可以用這種形式調用基類的任何函數(無論該函數是不是被派生類覆寫,是不是正在定義的這個函數)。同樣需要注意,在這種調用中,通常需要把表示本對象的self作為函數調用的第一個實參。
方法查找
如果從一個派生類的執行個體對象出發去調用方法,python解釋器需要确定應該調用哪個函數(在哪個類裡定義的函數)。查找過程從執行個體對象所屬的類開始,如果在這裡找到,就采用相應的函數定義;如果沒找到就到這個類的基類裡找。這個過程沿着繼承關系繼續進行,在某個類裡找到所需要的函數後就使用它。如果查找過程進行到已經沒有可用的基類,最終也沒找到所需函數屬性,那就是屬性無定義,python解釋器将報告attributeerror異常。python解釋器處理派生類的定義時,将在構造出的類對象裡記錄其基類的資訊,以支援使用這個類(及其對象)時的屬性查找。
如前所述,定義派生類時可以覆寫基類裡已有的函數定義(也就是說,重新定義一個同名函數)。按照上述查找過程,一旦某函數在派生類裡重新定義,在其執行個體對象的方法調用解析中,就不會再去使用基類裡原來定義的方法了。
假設在某個執行個體對象調用的一個方法f裡調用了另一個方法g,而且後一方法也是基于這個執行個體對象調用的(通過self.g(...))。在這種情況下,查找方法g的過程就隻與這個執行個體對象(的類型)有關,與前一方法f是在哪個類裡定義的情況無關。
考慮一個執行個體。假定b是c的基類,兩個類的定義分别是:
如果在建立b類的執行個體對象x之後調用x.f(),顯然将調用b類裡定義的g并列印出 “b.g called.”。但如果建立一個c類的執行個體對象y并調用y.f()呢?
由于c類裡沒有f的定義,y.f()實際調用的是b類裡定義的f。由于在f的定義裡出現了調用self.g,現在出現了一個問題:如何确定應該調用的函數g。從程式的正文看,正在執行的方法f的定義出現在類b裡,在類b裡,self的類型應該是b。如果根據這個類型去查找g,就應該找到類b裡定義的函數g。采用這種根據靜态程式正文去确定被調用方法的規則稱為靜态限制(另一常見說法是靜态綁定)。但python不這樣做,它和多數常見的面向對象語言一樣,基于方法調用時self所表示的那個執行個體對象的類型去确定應該調用哪個g,這種方式稱為動态限制。
這樣,y.f()的執行過程将是:由于y是值是c類的執行個體對象,首先基于它确定實際應該調用的方法函數f。由于c類裡沒有f的定義,按規則應該到c類的基類中去查找f。在c的基類b裡找到了f的定義,是以應該執行它。下一個問題是在函數f的執行中遇到了調用self.g()。由于當時self的值是一個c類的執行個體對象,确定g的工作再次從調用對象所屬的c類開始進行。由于c類裡存在函數g的定義,它就是應該調用的方法,執行這個方法函數将列印出“c.g called.”。
在程式設計領域,這種通過動态限制确定調用關系的函數稱為虛函數。
标準函數super()
python提供了一個内置函數super,把它用在派生類的方法定義裡,就是要求從這個類的直接基類開始做屬性檢索(而不是從這個類本身開始查找)。采用super函數而不直接寫具體基類的名字,産生的查找過程更加靈活。如果直接寫基類的名字,無論在什麼情況下執行,總是調用該基類的方法,而如果寫super(),python解釋器将根據目前類的情況去找到相應的基類,自動确定究竟應該使用哪個基類的屬性。
函數super有幾種使用方式,最簡單的是不帶參數的調用形式,例如
super().m(...)
如果在一個方法函數的定義裡出現這個調用語句,執行到這個語句時,python解釋器就會從這個對象所屬類的基類開始,按照上面介紹的屬性檢索規則去查找函數m。下面是一段用于說明相關問題的簡單代碼:
如果執行類c2裡的m1,python解釋器将從c2的基類開始找m1(也就是說,從c1開始查找)。由于c1裡有m1的定義,最終調用的是c1裡的函數m1。顯然,這種形式的super函數調用(并進而調用基類的某方法函數)隻能出現在方法函數的定義裡。在實際調用時,目前執行個體将被作為被調用函數的self實參。
函數super的第二種使用形式是super(c,obj).m(...),這種寫法要求從指定的類c的基類開始查找函數屬性m,調用裡出現的obj必須是類c的一個執行個體。python解釋器找到函數m後将用obj作為該函數的self實參。這種寫法可以出現在程式的任何地方,并不要求一定出現在類的方法函數裡。
函數super其他調用形式的使用情況更特殊,這裡就不詳細介紹了。