來自《python學習手冊第四版》第六部分
python
5、運算符重載(29章)程序員
這部分深刻介紹更多的細節並看一些經常使用的重載方法,雖然不會展現每種可用的運算符重載方法,可是這裏給出的代碼也足夠覆蓋python這一類功能的全部可能性。運算符重載只是意味着在類方法中攔截內置的操做,當類的實例出如今內置操做中,python自動調用咱們本身的方法,而且返回值變成了相應操做的結果:a、運算符重載讓類攔截常規的Python運算;b、類能夠重載全部Python表達式運算符;c、類也可重載打印、函數調用、屬性點號運算等內置運算;d、重載使類實例的行爲像內置類型;e、重載是經過提供特殊名稱的類方法來實現的。算法
一、構造函數和表達式:__init__和__sub__。來舉個簡單的重載例子。例如,下面文件number.py內的Number類提供一個方法來攔截實例的構造函數(__init__),此外還有一個方法捕捉減法表達式(__sub__)。這種特殊的方法是鉤子,可與內置運算相綁定:數據庫
該代碼所見到的__init__構造函數是python中最經常使用的運算符重載方法,它存在與絕大多數類中。編程
二、常見的運算符重載方法:在類中,對內置對象(例如,整數和列表)所能作的事,幾乎都有相應的特殊名稱的重載方法。表29-1列出其中一些最經常使用的重載方法。事實上,不少重載方法有好幾個版本(例如,加法就有__add__、__radd__和__iadd__):
設計模式
全部重載方法的名稱先後都有兩個下劃線字符,以便把同類中定義的變量名區別開來。特殊方法名稱與表達式惑運算的映射關係,是由python語言預先定義好的(在標準語言手冊中有說明)。例如:名稱__add__按照python語言的定義,不管__add__方法的代碼實際在作些什麼,老是對應到了表達式+。若是沒有定義運算符重載方法的話,它可能繼承自超類,就像任何其餘的方法同樣。運算符重載方法也都是可選的,若是沒有編寫或繼承一個方法,類直接不支持這些運算,而且視圖使用它們會引起一個異常。一些內置操做,如打印,有默認的重載方法(繼承自3.0中隱含object類),可是,若是沒有給出相應的運算符重載方法的話,大多數內置函數會對類實例失效。多數重載方法只用在須要對象行爲表現的就像內置類型同樣的高級程序中。api
三、索引和分片:__getitem__和__setitem__。若是在類中定義了(惑繼承了)的話,則對於實例的索引運算,會自動調用__getitem__。當實例X出如今X【i】這樣的索引運算中時,python會調用這個實例繼承的__getitem__方法(若是有的話),把X做爲第一個參數傳遞,而且方括號內的索引值傳給第二個參數。例如,下面的類將返回索引值的平方:安全
四、攔截分片:除了索引,對於分片表達式也調用__getitem__。正式的說,內置類型以一樣的方式處理分片。例如,下面是在一個內置列表上工做的分片,使用了上邊界和下邊界以及一個stride:服務器
實際上,分片邊界綁定到了一個分片對象中,而且傳遞給索引的列表實現。實際上,咱們老是能夠手動的傳遞一個分片對象,分片語法主要是用一個分片對象進行索引的語法糖:網絡
對於帶有一個__getitem__的類,這是很重要的,該方法將既針對基本索引(帶有一個索引)調用,又針對分片(帶有一個分片對象)調用。咱們前面的類沒有處理分片,由於它的數學假設傳遞了整數索引,可是,以下類將會處理分片。當針對索引調用的時候,參數像前面同樣是一個整數:
當針對分片調用的時候,方法接收一個分片對象,它在一個新的索引表達式中直接傳遞給嵌套的列表索引:
若是使用的話,__setitem__索引賦值方法相似的攔截索引和分片賦值,它爲後者接收了一個分片對象,它可能以一樣的方式傳遞到另外一個索引賦值中:
五、實際上,__getitem__可能在甚至比索引和分片更多的環境中自動調用,在3.0以前,類也能夠定義__getslice__和__setslice__方法來專門攔截分片獲取和賦值;它們將傳遞一系列的分片表達式,而且優先於__getitem__和__setitem__用於分片。這些特定於分片的方法已經從3.0中移除了,所以,應該使用__getitem__和__setitem__來替代,以考慮到索引和分片對象均可能做爲參數。在大多數類中,這不須要任何特殊的代碼就能工做,由於索引方法能夠在另外一個索引表達式的方括號中傳遞分片對象(就像例子中那樣)。此外,不要混淆了3.0中用於索引攔截的__index__方法;須要的時候,該方法針對一個實例返回一個整數值,供轉化爲數字字符串的內置函數使用:
儘管這個方法並不會攔截像__getitem__這樣的實例索引,但它也能夠在須要一個整數的環境中應用--包括索引:
該方法在2.6中以一樣的方式工做,只不過它不會針對hex和oct內置函數調用(在2.6中使用__hex__和__oct__來攔截這些調用)。
六、索引迭代:__getitem__。新手可能暫時不太領會這裏的技巧。但這些技巧都很是有用。for語句的做用是從0到更大的索引值,重複對序列進行索引運算,直到檢測到超出邊界的衣長。所以,__getitem__也能夠是python中一種重載迭代的方式。若是定義了這個方法,for循環每次循環時都會調用類的__getitem__,並持續搭配有更高的偏移值:任何會響應索引運算的內置惑用戶定義的對象,一樣會響應迭代:
任何支持for循環的類也會自動支持python全部迭代環境,而其中多種環境在前幾章看過了。例如,成員關係測試 in 、列表解析、內置函數map、列表和元組賦值運算以及類型構造方法也會自動調用__getitem__(若是定義了的話):
在實際應用中,這個技巧可用於創建提供序列接口的對象,並新增邏輯到內置的序列類型運算。第31章會擴展內置類型的。
七、迭代器對象:__iter__和__next__。雖然上一節的__getitem__技術有效,但它真的只是迭代的一種退而求其次的方法。現在python中全部的迭代環境都會先嚐試__iter__方法,在嘗試__getitem__。也就是說,它們寧願使用第14章所學到的迭代協議,而後纔是重複對對象進行索引運算。只有在對象不支持迭代協議的時候,纔會嘗試索引運算。通常來講,也應該有限使用__iter__,它可以比__getitem__更好的支持通常的迭代環境。從技術上來講,迭代環境是經過調用內置函數iter去嘗試尋找__iter__方法來師兄的,而這種方法應該返回一個迭代器對象。若是已經提供了,python會重複調用這個迭代器對象的next方法,直到發生StopIteration異常。若是沒找到這類__iter__方法,python會改用__getitem__機制,就像以前那樣經過偏移量重複索引,知道引起IndexError異常(對於手動迭代來講,一個next內置函數也能夠很方便的使用:next(I) 與I.__next__()是相同的)。ps:在2.6中叫作I.next(),而在3.0中叫作I.__next__()。
八、用戶定義的迭代器。在__iter__機制中,類就是經過實現第14章和第20章介紹的迭代其協議,來實現用戶定義的迭代器的。例如,下面的文件iters.py,定義了用戶定義的迭代器類來生成平方值:
這裏,迭代器對象就是實例self,由於next方法是這個類的一部分。在較爲複雜的場景中,迭代器對象可定義爲個別的類或有本身的狀態信息的對象,對相同數據支持多種迭代。以python raise語句發出信號表示迭代結束。手動迭代對內置類型也有效:
__getitem__所寫的等效代碼可能不是很天然,由於for會對全部的0和較高值的偏移值進行迭代。傳入的偏移值和所產生的值的範圍只有間接的關係(O...N須要映射爲start...stop)。由於__iter__對象會在調用過程當中明確的保留狀態信息,因此比__getitem__具備更好的通用性。另外,有時__iter__迭代其會比__getitem__更復雜和難用。迭代器是用來迭代,不是隨機的索引運算。事實上,迭代器根本沒有重載索引表達式:
__iter__機制也是咱們在__getitem__中所見到的其餘全部迭代環境的實現方式(成員關係測試、類型構造函數、序列賦值運算等)。然而,和__getitem__不一樣的是,__iter__只循環一次,而不是循環屢次。例如,Squares類只循環一次,循環以後就變爲空。每次新的循環,都得建立一個新的迭代器對象:
ps:若是用生成器函數編寫,這個例子可能更簡單:
和類不一樣,這個函數會自動在迭代中存儲其狀態。固然,這是假設的例子。實際上,能夠跳過這兩種技術,只用for循環、map或是列表解析,一次建立這個列表。在python中,完成任務最佳並且最快的方法一般也是最簡單的方法:
不過在模擬更復雜的迭代時,類會比較好用。
九、有多個迭代器的對象:以前,提到過迭代器對象能夠定義成一個獨立的類,有其本身的狀態信息,從而可以支持相同數據的多個迭代。考慮一下,當步進到字符串這類內置類型時,會發生什麼:
在這裏,外層循環調用iter從字符串中取得迭代器,而每一個嵌套的循環也作相同的事來得到獨立的迭代器。由於每一個激活狀態下的迭代其都有本身的狀態信息,而無論其餘激活狀態下的循環是什麼狀態。在前面的1四、20章都有,例如,生成器函數和表達式。以及map和zip這樣的內置函數,都證實是單迭代對象;相反,range內置函數和其餘的內置類型(如列表),支持獨立位置的多個活躍迭代器。當用類編寫用戶定義的迭代器時,由咱們來決定是支持一個單個的仍是多個活躍的迭代。要達到多個迭代器的效果,__iter__只需替迭代器定義新的狀態對象,而不是返回self。下面定義了一個迭代器,迭代時,跳過下一個元素。由於迭代器對象會在每次迭代時都從新建立,因此可以支持多個處於激活狀態下的循環:
運行時,這個例子工做的和對內置字符串進行嵌套循環同樣,由於每一個循環都會得到獨立的迭代器對象來記錄本身的狀態信息,因此每一個激活狀態下的循環都有本身在字符串中的位置:
做爲對比,除非咱們在嵌套循環中再次調用Squares來得到新的迭代對象,不然以前的Squares例子只支持一個激活狀態的迭代。這裏,只有SkipObject,但從該對象中建立了許多的迭代器對象。能夠使用內置工具達到相似的效果,例如,用第三參數邊界值進行分片運算來跳過元素:
不過這並不相同,由於:a、這裏的每一個分片表達式,實質上是一次把結果列表存儲在內存中;b、迭代器則是一次產生一個值,這樣使大型結果列表節省了實際的空間。其次,分片產生的新對象,其實咱們沒有對同一個對象進行多處的循環。爲了更接近類,須要事先建立一個獨立的對象經過分片運算進行步進:
這樣與基於類的解決方法更類似一些,可是,它還是一次性把分片結果存儲在內存中(目前內置分片運算並無生成器),而且只等效於這裏跳過一個元素的特殊狀況。由於迭代器可以作類能作的任何事,因此它比例子所展示的更通用。不管應用程序是否須要這種通用性,用戶定義的迭代器都是強大的工具,可讓咱們把任意對象的外觀和用法變得很像本教程所遇到的其餘序列和可迭代對象。例如,能夠將這項技術用在數據庫對象中,經過迭代進行數據庫的讀取,讓多個遊標進入同一個查詢結果。
十、成員關係:__contains__、__iter__和__geitiem__:迭代器的內容比目前看到的還豐富,運算符重載每每是多個層級的:類能夠提供特定的方法,或者用做退而求其次的更通用的替代方法,例如:a、2.6中的比較使用__It_這樣的特殊方法來表示少於比較(若是有的話)、或者使用通用的__cmp__。3.0只使用特殊的方法,而不是__cmp_-,如後面說道的;b、布爾測試相似於先嚐試一個特定的__bool__(以給出一個明確的Ture/False結果),而且,若是沒有它,將會退而求其次到更通用的__len__(一個非零的長度意味着True)。正如後面見到的,2.6也同樣起做用,可是,使用名稱__nonzero__而不是__bool__。在迭代中,類一般把in成員關係運算符實現爲一個迭代,使用__iter__方法或__getitem__方法。要支持更加特定的成員關係,類可能編寫一個__contains__方法,當出現的時候,該方法優先於__iter__方法,__iter__方法優先於__getitem__方法。__contains__方法應該把成員關係定義爲對一個映射應用鍵,以及用於序列的搜索。考慮下面的類,它編寫了全部3個方法和測試成員關係以及應用於一個實例的各類迭代環境。調用的時候,其方法會打印出跟蹤消息:
(print和if在同一個層級)。
這段腳本運行的時候,器輸出以下所示,特定的__contains__攔截成員關係,通用的__iter__捕獲其餘的迭代環境以致__next__重複地被調用,而__getitem__不會被調用:
可是,要觀察若是註釋掉__contains__方法後代碼的輸出發生了什麼,成員關係如今路由到了通用的__iter__:
正如咱們所看到的,__getitem__方法甚至更通用:除了迭代,它還攔截顯示索引和分片。分片表達式用包含邊界的一個分片對象來觸發__getitem__,既針對內置類型,也針對用戶定義的類,所以,咱們的類中分片是自動化的:
然而,在並不是面向序列的、更加現實的迭代用例中,__iter__方法可能很容易編寫,由於它沒必要管理一個整數索引,而且__contains__考慮到做爲一種特殊狀況優化成員關係。
十一、屬性引用:__getattr__和__setattr__。__getattr__方法是攔截屬性點號運算。更具體的說,當經過對未定義屬性名稱和實例進行點號運算時,就會用屬性名稱做爲字符串調用這個方法。若是python能夠經過其繼承樹搜索流程找到這個屬性,該方法就不會被調用。由於有這種狀況,因此__getattr__能夠做爲鉤子來經過通用的方式響應屬性請求。例子以下:
這裏,empty類和其實例X自己並無屬性,因此對X.age的存取會轉至__getattr__方法,self則賦值爲實例(X),而attrname則賦值爲未定義的屬性名稱字符串(「age」)。這個類傳回一個實際值做爲X.age點號表達式的結果(40),讓age看起來像實際的屬性。實際上,age變成了動態計算的屬性。當類不知道該如何處理的屬性,__getattr__會引起內置的AttributeError異常,告訴python,那真的是未定義屬性名。請求X.name時,會引起錯誤。當在後兩章看到實際的委託和內容屬性時,會再看到__getattr__。
十二、接上11.有個相關的重載方法__setattr__會攔截全部屬性的賦值語句。若是定義了這個方法,self.attr=value會變成self.__setattr__('attr',value)。這一點技巧性很高,由於在__setattr__中對任何self屬性作賦值,都會再調用__setattr__,致使了無窮遞歸循環(最後就是堆棧溢出異常)。若是想使用這個方法,要肯定是經過對屬性字典作索引運算來賦值任何實例屬性的。也就是說,是使用self.__dict__['name'] = x,而不是self.name = x。
1三、其餘屬性管理工具:a、__getattribute__方法攔截全部的屬性獲取,而不僅是那些未定義的,可是,當使用它的時候,必須比使用__getattr__更當心的避免循環;b、Property內置函數運行咱們把方法和特定類屬性上的獲取和設置操做關聯起來;c、描述符提供了一個協議,把一個類的__get__和__set__方法與對特定類屬性的訪問關聯起來。在第31章介紹,並在第37章詳細的介紹全部屬性管理技術。
1四、模擬實例屬性的私有性:第一部分。下面代碼把上一個例子通用化了,讓每一個子類都有本身的私有變量列表,這些變量名沒法經過其實例進行賦值:
實際上,這是python中實現屬性私有性(也就是沒法在類外對屬性名進行修改)的首選方法。雖然python不支持private聲明,但相似這種技術能夠模擬其主要的目的。不過,這只是一部分的解決方案。爲使其更有效,必須加強它的功能,讓子類也可以設置私有屬性,而且使用__getattr__和包裝(有時候稱爲代理)來檢測對私有屬性的讀取。在第38章將會介紹類裝飾器來更加通用的攔截和驗證屬性,即便私有性能夠以此方式模擬,但實際應用中幾乎不會這麼作。
1五、__repr__和__str__會返回字符串表達式形式。下一個例子是已經見過的__init__構造函數和__add__重載方法,也會定義返回實例的字符串表達形式的__repr__方法。字符串格式把self.data對象轉換爲字符串。若是定義了話,當類的實例打印或轉換成字符串時__repr__(或其近親__str__)就會自動調用。這些方法可替對象定義更好的顯示格式,而不是使用默認的實例顯示。實例對象的默認顯示既無用也很差看:
可是編寫或繼承字符串表示方法容許咱們定製顯示:
兩個顯示的方法,是爲了進行用戶友好的顯示。具體的說:a、打印操做會首先嚐試__str__和str內置函數(print運行的內部等價形式)。它一般應該返回一個用戶友好的顯示;b、__repr__用於全部其餘的環境中:用戶交互模式下提示迴應以及repr函數,若是沒有使用__str__,會使用print和str。它一般應該返回一個編碼字符串,能夠用來從新建立對象,或者給開發者一個詳細的顯示。__repr__用於任何地方,除了當定義了一個__str__的時候,使用print和str。不過若是沒有定義__str__,打印仍是使用__repr__,但反過來並不成立,其餘環境,例如,交互式響應模式,只是使用__repr__,而且根本不要嘗試__str__:
正是由於這一點,若是想讓全部環境都有統一的顯示,__repr__是最佳選擇。不過經過分別定義這兩個方法,就能夠在不一樣環境內支持不一樣顯示。例如,終端用戶顯示使用__str__,而程序員在開發期間則使用底層的__repr__來顯示。實際上,__str__只是覆蓋了__repr__以獲得用戶友好的顯示環境:
這裏提到的兩種用法。首先,記住_-str__和__repr_-都必須返回字符串;其餘的結果類型不會轉換並會引起錯誤,所以,若是必要的話,確保用一個轉換器處理它們。其次根據一個容器的字符串轉換邏輯,__str__的用戶友好的顯示可能只有當對象出如今一個打印操做頂層的時候才應用,嵌套到較大對象中的對象可能用其__repr__或默認方法打印。以下代碼說了:
爲了確保一個定製顯示在全部的環境中都顯示而無論容器是什麼,能夠編寫__repr__,而不是__str__;前者在全部的狀況下都運行,即使後者不適用的狀況也是如此:
在實際應用中,除了__init__之外,__str__(或其近親__repr__)彷佛是python腳本中第二個最經常使用的運算符重載方法。在能夠打印對象而且看見定製顯示的任什麼時候候,可能就是使用這兩個之一的工具。
1六、右側加法和原處加法:__radd__和__iadd__。從技術上說,前面的例子中出現的__add__方法並不支持+運算右側使用實例對象。要實現這類表達式,而支持可互換的運算符,能夠一併編寫__radd__方法。只有當+右側的對象是類實例,而左邊對象不是類實例時,python纔會調用__radd__。在其餘全部狀況下,則由左側對象調用__add__方法:
__radd__中的順序與之相反:self 是在+的右側,而other是在左側。此外,注意 x 和 y 是同一個類的實例。當不一樣類的實例混合出如今表達式時,python優先選擇左側的那個類。當兩個實例相加的時候,python運行__add__,它反過來經過簡化左邊的運算數來觸發__radd__。在更爲實際的類中,其中類類型可能須要在結果中傳播,事情可能變得更須要技巧:類型測試可能須要辨別它是否可以安全的轉換並由此避免嵌套。例如:下面的代碼中若是沒有isinstance測試,當兩個實例相加而且__add__觸發__radd__的時候,咱們最終獲得一個Computer,其val是另外一個Commuter:
1七、原地加法:爲了也實現+=原處擴展相加,編寫一個__iadd__或__add__。若是前者空缺的話,使用後者。實際上,前面小節的Commuter類爲此已經支持+=了,可是__iadd__考慮到了更加高效的原處修改:
每一個二元運算都有相似的右側和原處重載方法,它們以相同的方式工做(例如:__mul__,__rmul__和__imul__)。右側方法在實際中不多用到:只有在須要運算符具備交換性的時候,纔會編寫它們,而且只有在真正須要支持這樣的運算符的時候,纔會使用。例如,一個Vector類可能使用這些工具,可是一個Employee或Button類可能不會。
1八、call表達式__call__。當調用實例時,使用__call__方法。這不是循環定義:若是定義了,python就會爲實例應用函數調用表達式運行__call__方法。這樣可讓類實現的外觀和用法相似於函數:
更正式的說,在第18章介紹的全部參數傳遞方式,__call__方法都支持,傳遞給實例的任何內容都會傳遞給該方法,包括一般隱式的實例參數。例如,方法定義:
都匹配以下全部的實例調用:
直接效果就是,帶有一個__call__的類和實例,支持與常規函數和方法徹底相同的參數語法和語義。像這樣的攔截調用表達式容許類實例模擬相似函數的外觀,可是,也在調用中保持了狀態信息以供使用:
在這個示例中,__call__乍一看可能有點怪,一個簡單的方法能夠提供相似的功能:
然而,當須要爲函數的API編寫接口時,__call__就變得頗有用:這能夠編寫遵循所須要的函數來調用接口對象,同時又能保留狀態信息。事實上,這多是除了__init__構造函數以及__str__和__repr__顯示格式方法外,第三個最經常使用的運算符重載方法了。
1九、函數接口和回調代碼。(這裏以python安裝的時候附帶的tkinter gui工具箱爲例)tkinter gui工具箱能夠將函數註冊成事件處理器(也就是回調函數callback)。當事件發生時,tkinter會調用已註冊的對象。若是想讓事件處理器保存事件之間的狀態,能夠註冊類的綁定方法或者遵循所需接口的實例(使用__call__)。在這一節的代碼中,第二個例子中的x.comp和第一個例子中的x均可以用這種方式做爲相似於函數的對象傳遞。這裏舉個假設__call__例子,應用於gui領域。下列類定義了一個對象,支持函數調用接口,可是也有狀態信息,可記住稍後按下按鈕後應該變成什麼顏色:
如今在gui環境中,即便這個gui期待的事件處理器是無參數的簡單函數,仍是能夠爲按鈕把這個類的實例註冊成事件處理器:
當這個按鈕按下時,會把實例對象單詞簡單的函數來調用,就像下面的調用同樣。不過,因它把狀態保留成實例的屬性,因此知道應該作什麼:
實際上,這多是python語言中保留狀態信息的最好方式,比以前針對函數所討論的技術更好(全局變量、嵌套函數做用域引用以及默承認變參數等)。利用oop,狀態的記憶是明確的使用屬性賦值運算而實現的。python程序員偶爾還會用兩種其餘方式,把信息和回調函數聯繫起來。其中一個選項是使用lambda函數的默認參數:
另外一種是使用類的綁定方法:這種對象記住了self實例以及所引用的函數,使其能夠在稍後經過簡單的函數調用而不須要實例來實現:
當按鈕按下時,就好像是gui這麼作的,啓用changeColor方法來處理對象的狀態信息:
這種技巧較爲簡單,比起__call__重載就不通用了。__call__可以讓咱們把狀態信息附加在可調用對象上,因此天然而然的成爲了被一個函數記住並調用了另外一個函數的實現技術。
20、比較:__It__、__gt__和其餘方法。正如表29-1所示,類能夠定義方法來捕獲全部的6種比較運算符:<、>、<=、>=、==、!=。這些方法一般很容易使用,可是有下面的這些限制:a、與前面討論的__add__、__radd__對不一樣,比較方法沒有右端形式。相反,當只有一個運算數支持比較的時候,使用其對應方法(例如,__It__和__gt__互爲對應);b、比較運算符沒有隱式關係。例如,==並不意味着 != 是假的,所以,__eq__和__ne__應該定義爲確保兩個運算符都正確的做用;c、在2.6中,若是沒有定義更爲具體的比較方法的話,對全部比較使用一個__cmp__方法。它返回一個小於、等於或大於0的數,以表示比較其兩個參數(self和另外一個操做數)的結果。這個方法每每使用cmp(x,y)內置函數來計算其結果。__cmp__方法和cmp內置函數都從3.0中刪除了:使用更特定的方法來替代。做爲一個快速介紹,考慮以下的類和測試代碼:
在3.0和2.6下運行的時候,末尾的打印語句顯示它們的註釋中提到的結果,由於該類的方法攔截並實現了比較表達式。
2一、2.6的__cmp__方法(已經從3.0中移除了)。在2.6中,若是沒有定義更加具體的方法的話,__cmp__方法做爲一種退而求其次的方法:它的整數結果用來計算正在運行的運算符。例如:以下的代碼在2.6下產生一樣的結果,可是在3.0中失敗,由於__cmp__再也不可用:
這在3.0中失效是由於__cmp__再也不特殊,而不是由於cmp內置函數再也不使用。若是咱們把前面的類修改成以下的形式,以試圖模擬cmp調用,那麼代碼將在python2.6中工做,但在3.0下無效:
2二、布爾測試:__bool__和__len__。正如前面提到的,類可能也定義了賦予其實例布爾特性的方法,在布爾環境中,python首先嚐試__bool__來獲取一個直接的布爾值,而後,若是沒有該方法,就嘗試__len__類根據對象的長度肯定一個真值。一般,首先使用對象狀態或其餘信息來生成一個布爾結果:
若是沒有這個方法,python退而求其次的求長度,由於一個非空對象看做是真(如,一個非零長度意味着對象是真的,而且一個零長度意味着它爲假):
若是兩個方法都有,python喜歡__bool__賽過__len__,由於它更具體:
若是沒有定義真的方法,對象毫無疑問的看做爲真:
2三、2.6中的布爾。對於2.6的用戶應該在第22中的全部代碼中使用__nonzero__而不是__bool__。3.0把2.6的__nonzero__方法從新命名爲__bool__,當布爾測試以相同的方式工做(3.0和2.6都是用__len__做爲候補)。若是沒有使用2.6的名稱,本節中第一個測試將會一樣的工做,可是,僅僅是由於__bool__在2.6中沒有識別爲一個特殊的方法名稱,而且對象默認看做是真的。:
這在3.0中像宣傳的那樣有效。然而,在2.6中,__bool__被忽視而且對象老是看做是真:
在2.6中,針對布爾值使用__nonzero__(或者從設置爲假的__len__候補方法返回0):
不過,__nonzero__只在2.6中有效;若是在3.0中使用,它將默認的忽略,而且對象將被默認的分類爲真,就像是在2.6中使用__bool_-同樣。
2四、對象析構函數:__del__。每當實例產生時,就會調用__init__構造函數。每當實例空間被收回時(在垃圾收集時),它的對立面__del__,也就是析構函數,就會自動執行:
這裏,當brian賦值爲字符串時,咱們會失去life實例的最後一個引用。所以會觸發器析構函數,。這樣能夠用於一些清理行爲(例如,中斷服務器的鏈接)。然而,基於某些緣由,在python中,析構函數不像其餘oop語言那麼經常使用:a、由於python在實例收回時,會自動收回該實例所擁有的全部空間,對於空間管理來講,是不須要析構函數的;b、沒法輕易的預測實例什麼時候收回,一般最好是在有意調用的方法中(try/finally語句)編寫代碼去終止活動。在某種狀況下,系統表中可能還在引用該對象,使析構函數沒法執行。ps:實際上,__del__可能會很難使用。例如,直接向sys.stderr打印一條警告消息,而不是觸發一個異常事件,這也會從中引起異常,由於垃圾收集器在不可預料的環境下運行。此外,當咱們期待垃圾收集的時候,對象間的循環引用可能會阻止其發生。一個可選的循環檢測器,是默承認用的,最終能夠自動檢測這樣的對象,可是,只有在它們沒有__del__方法的時候纔可用。可參見python標準手冊對__del__和gc 垃圾收集模塊的介紹。
6、類的設計(30章)
這裏介紹一些核心的oop概念,以及一些比目前展現過的例子更實際的額外例子。這裏有:繼承、組合、委託、工廠;類設計的概念,僞私有屬性、多繼承和邊界方法,這部分簡單介紹,更詳細的須要參考一些oop設計的書籍。
一、python的oop實現能夠歸納爲三個概念,:a、繼承,繼承是基於python中的屬性查找的(在X.name表達式中);b、多態,在X.method方法中,metohd的意義取決於X的類型(類);c、封裝,方法和運算符實現行爲,數據隱藏默認是一種慣例。以前屢次介紹了python中的多態;這是由於python沒有類型聲明而出現的。由於屬性老是在運行期解析,實現相同接口的對象是可互相交換的,因此客戶端不須要知道實現它們調用的方法的對象種類。
二、經過調用標記進行重載(或不要):有些oop語言把多態定義成基於參數類型標記(type signature)的重載函數。可是,由於python中沒有類型聲明,因此這種概念行不通,python中的多態是基於對象接口的,而不是類型。下面是經過參數列表進行重載方法:
這樣的代碼是會執行的,可是,由於def只是在類的做用域中把對象賦值給變量名,這個方法函數的最後一個定義纔是惟一保留的(就好像X=1,而後X=2,結果X將是2)。基於類型的選擇,能夠使用第四、9章見過的類型測試的想法去編寫代碼,或者使用低18章的參數列表工具:
一般來講,不須要這麼作,第16章說的,應該將程序寫成預期的對象接口,而不是特定的數據類型:
三、oop和繼承:「是一個(is -a)」關係。這裏舉個例子,開一家批薩店,須要聘請員工,並且須要創造一個機器人制做批薩,不過也會將機器人看做有薪水的功能齊全的員工。該團隊能夠經過文件employees.py中的四個類來定義。最通用的類Employee提供共同行爲,例如,加薪(giveRaise)和打印(__repr__)。員工有兩種,因此Employee有兩個子類:Chef和Server。這兩個子類都會覆蓋繼承的work方法來打印更具體的信息。最後,匹薩機器人是由更具體的類來模擬:PizzaRobot是一種Chef,也是一種Employee。以oop來講,成這些關係爲"is-a"連接:機器人是一個主廚,而主廚是一個員工,如下是employees.py文件:
當執行此模塊中的自我測試代碼時,會建立一個名爲bob的製做匹薩機器人,從三個類繼承變量名:PizzaRobot、Chef、Employee。例如,打印bob會執行Employee.__repr__方法,而給予bob加薪,則會運行Employee.giveRaise,由於繼承會在這裏找到這個方法:
在這樣的類層次中,一般能夠建立任何類的實例,而不僅是底部的類。例如,這個模塊中自我測試程序代碼的for循環,建立了四個類的實例。要求工做時,每一個反應都不一樣,由於work方法都各不相同。其實,這些類只是模仿真實世界的對象。work在這裏只打印信息。
四、oop和組合:「has-a」關係。對程序員來講,組合就是把其餘對象嵌入容器對象內,並使其實現容器方法;對設計師而言,組合是另外一種表示問題領域中關係的方式。可是組合不是集合的成員關係,而是組件,也就是總體的組成部分。組合也反映了各組成部分之間的關係,一般稱爲「has-a」關係,有些oop設計書籍把組合稱爲聚合(aggregation),或者使用聚合描述容器和所含物之間較弱的依賴關係來區分這兩個術語。「組合」就是指內嵌對象集合體。組合類通常都提供本身的接口,並經過內嵌的對象來實現接口。對於這個例子來講,就是有烤爐,服務生,主廚。顧客下單時:服務生接單,主廚製做匹薩等。下面的文件pizzashop.py文件:
PizzaShop類是容器和控制器,其構造函數會建立上一節所編寫的員工類實例並將其嵌入。此外,Oven類也在這裏定義。當此模塊的自我測試程序代碼調用PizzaShoprder方法時,內嵌對象會按照順序進行工做。注意:每份訂單建立了新的Customer對象,並且把內嵌的Server對象傳給Customer方法。顧客是流動的,可是,服務生是匹薩店的組成部分,另外,員工也涉及了繼承關係,組合和繼承是互補的工具。當執行這個模塊時,匹薩店處理兩份訂單:一份來自Homer,一份來自Shaggy:
這只是個用來模擬的例子,可是,對象和交互足以表明組合的工做。簡明的原則就是,類能夠表示任何用一句話表達的對象和關係。只要用類取代名詞,方法取代動詞。
五、重訪流處理器:就更爲現實的組合範例而言,能夠回憶第22章的oop,寫的通用數據流處理器函數的部分代碼:
這裏,不是使用簡單函數,而是編寫類,使用組合機制工做,來提供更強大的結構並支持繼承。下面的文件streams.py示範了一種編寫類的方式:
這個類定義了一個轉換器方法,期待子類來填充。以這種方式編碼,讀取器和寫入器對象會內嵌在類實例當中(組合),咱們在子類內提供轉換器的邏輯,而不是傳入一個轉換器函數(繼承)。文件converters.py:
在這裏,Uppercase類繼承了類處理的循環邏輯(以及其超類內縮寫的其餘任何事情)。它只需定i其所特有的事件:數據轉換邏輯。當這個文件執行時,會建立並執行實例,而該實例再從文件spam.txt中讀取,把該文件對應的大寫版本輸出到stdout流:
要處理不一樣種類的流,能夠把不一樣種類的對象傳入類的構造調用中。在這裏,使用了輸出文件,而不是流:
可是,就像以前說的,能夠傳入包裝在類中的任何對象(該對象定義了所須要的輸入和輸出方法接口),下面的是傳入寫入器類:
計時原始的Processor超類內的核心處理邏輯什麼也不知道,若是跟隨這個例子的控制流程,就會發現獲得了大寫轉換(經過繼承)以及HTML(經過組合)。處理代碼只在乎寫入器的write方法,並且又定義一個名爲convert的方法,並不在乎這些調用在作什麼。這種邏輯的多態和封裝遠超過類的威力。Processor超類只提供文件掃描循環。後期能夠進行擴充。本教程的重點是繼承,不過在實際中,組合和繼承用的同樣多,都是組織類結果的方式,尤爲是在較大型系統中。
六、爲何要在乎:類和持續性。pickle和類實例結合起來使用效果很好,並且能夠促進類的通用用法,經過pickle或shelve一個類實例,咱們獲得了包含數據和邏輯的組合的數據存儲。例如,類實例能夠經過python的pickle或shelve模塊,經過單個步驟存儲到磁盤上,在第27章使用shelve來存儲類的實例,而對象的picke接口很容易使用:
pickle機制把內存中的對象轉換成序列化的字節流,能夠保存在文件中,也可經過網絡發送出去。解出pickle狀態則是從字節流轉換回同一個內存中的對象,shelve也相似,可是它會自動把對象pickle生成按鍵讀取的數據庫,而此數據庫會導出相似於字典的接口:
上例中,使用類來模擬員工意味着只需作一點工做,就能夠獲得員工和商店的簡單數據庫:把這種實例對象pickle至文件,使其在python程序執行時都可以永久保存:
這一次性的把整個符合的shop對象保存到一個文件中,爲了在另外一個會話惑程序中再次找回,只要一個步驟,實際上,以這種方式存儲的對象保存了狀態和行爲:
七、oop和委託:「包裝」對象。oo的程序員時常會談到委託(delegation),一般就是指控制器對象內嵌其餘對象,而把運算請求傳給那些對象。控制器負責管理工做,例如:記錄存取等。在python中,委託一般是以__getattr__鉤子方法實現的,由於這個方法會攔截對不存在屬性的讀取,包括類(有時稱爲代理類)能夠使用__getattr__把任意讀取轉發給被包裝的對象。包裝類包有被包裝對象的接口,並且本身也能夠增長其餘運算,例如trace.py:
__getattr__會獲取屬性名稱字符串。這個程序代碼利用getattr內置函數,以變量名字符串從包裹對象取出屬性:getattr(X,N)就像是X.N。只不過N是表達式,可運行時計算出字符串,而不是變量。事實上,getattr(X,N)相似於X.__dict__[N],但前者也會執行繼承搜索,就像X.N,而getattr(X,N)則不會。能夠使用這個模塊包裝類的作法,管理任何帶有屬性的對象的存取:列表、字典甚至是類和實例。在這裏,wrapper類只是在每一個屬性讀取時打印跟蹤消息,並把屬性請求委託給嵌入的wrapped對象:
實際效果就是以包裝類內額外的代碼來加強被包裝的對象的整個接口。能夠利用這種方式記錄方法調用,把方法調用轉給其餘惑定製的邏輯等等。ps:在2.6中運算符重載方法經過把內置操做導向__getattr__這樣的通用屬性攔截方法來運行。例如,直接打印一個包裝對象,針對__repr__或__str__調用該方法,隨後把調用傳遞給包裝對象。在3.0中,這種狀況再也不會發生:打印不會觸發__getattr__,而且使用一個默認顯示。在3.0中,新式類在類中查找運算符重載方法,而且徹底忽略常規的實例查找。
八、類的僞私有屬性。在第五部分中,知道每一個在模塊文件頂層賦值的變量名都會導出。在默認狀況下,類也是這樣:數據隱藏是一個慣例,客戶端能夠讀取惑修改任何它們想要的類或實例的屬性。事實上,用CPP術語來講,屬性都是「public」和"virtual",在任意地方均可進行讀取,而且在運行時進行動態查找。不過python支持變量名壓縮(mangling,至關於擴張)的概念,讓類內某些變量局部化。壓縮後的變量名有時會被誤認爲是「私有屬性」,但這其實只是一種把類所建立的變量名局部化的方式而已:名稱壓縮並沒有法阻止類外代碼對它的讀取。這種功能主要是爲了不實例內的命名空間的衝突,而不是限制變量名的讀取。所以,壓縮的變量名最好稱爲"僞私有",而不是"私有"。這個功能通常在多人的項目中編寫大型的類的層次,不然可能以爲沒什麼用,更通俗的說,python程序員用一個單個的下劃線來編寫內部名稱(例如,_X),這只是一個非正式的慣例,讓知道這是一個不該該修改的名字。
九、變量名壓縮概覽:變量名壓縮的工做方式:class語句內開頭有兩個下劃線,但結尾沒有兩個下劃線的變量名,會自動擴張,從而包含了類的名稱。例如像Spam類內_X這樣的變量名會自動變成_Spam__X:原始的變量名會在頭部加入一個下劃線,而後是所在類名稱,由於修改後的變量名包含了所在類的名稱,至關於變得獨特。不會和同一層級中其餘類所建立的相似變量名相沖突。變量名壓縮只發生在class語句內,並且只針對開頭有兩個下劃線的變量名。然而,每一個開頭有兩個下劃線的變量名都會發生這件事,包括方法名稱和實例屬性名稱(例如:在Spam類內,引用的self._X實例屬性會變成self._Spam_X).由於不止有一個類在給一個實例新增屬性,因此這種方法是有助於避免變量名衝突的。
十、爲何使用僞私有屬性。該功能是爲了緩和與實例屬性存儲方式有關的問題。在python中,全部實例屬性最後都會在類樹底部的單個實例對象內。這一點和cpp模型大不相同,cpp模型的每一個類都有本身的空間來存儲其所定義的數據成員。在類方法內,每當方法賦值self的屬性時(例如,self.attr = value),就會在該實例內修改或建立該屬性(繼承搜索只發生在引用時,而不是賦值時)。即便在這個層次中有多個類賦值相同的屬性,也是如此,所以有可能發生衝突。例如,假設當一位程序員編寫一個類時,他認爲屬性名稱X是在該實例中。在此類的方法內,變量名被設定,而後取出:
假設另外一位程序員獨立做業,對他寫的類也有一樣的假設:
這兩個類都在各行其事。若是這兩個類混合在相同類樹中時,問題就產生了:
如今,當每一個類說self.X時所獲得的值,取決於最後一個賦值是哪個類。由於全部對self.X的賦值語句都是引用一個i額相同實例,而X屬性只有一個(I.X),不管有多少類使用這個屬性名。爲了保證屬性會屬於使用它的類,可在類中任何地方使用,將變量名前加上兩個下劃線,如private.py這個文件所示:
當加上了這樣的前綴時,X屬性會擴張,從而包含它的類的名稱,而後才加到實例中。若是對 I 執行dir,或者在屬性賦值後查看其命名空間字典,就會看見擴張後的變量名_C1_X和_C2_X,而不是X。由於擴張讓變量名在實例內變得獨特,類的編碼者能夠安全的假設,他們真的擁有任何帶有兩個下劃線的變量名:
這個技巧可避免實例中潛在的變量名衝突,可是,這並非真正的私有。若是知道所在類的名稱,依然能夠使用擴張後的變量名(例如,I._C1_X = 77),在可以引用實例的地方,讀取這些屬性。另外一方面,這個功能也保證不太可能意外的訪問到類的名稱。僞私有屬性在較大的框架或工具中也是有用的,既能夠避免引入可能在類樹中某處偶然隱藏定義的新的方法名,也能夠減小內部方法被在樹的較低處定義的名稱替代的機會。若是一個方法傾向於只在一個可能混合到其餘類的類中使用,在前面使用雙下劃線,以確保該方法不會受到樹中的其餘名稱的干擾,特別是在多繼承的環境中:
在類頭部行中,超類按照它們從左到右的順序搜索。在這裏,就意味着Sub1首選Tool屬性,而不是Super中的那些屬性。儘管在這個例子中,咱們可能經過切換Sub1類頭部列出的超類的順序,來迫使python首先選擇應用程序類的方法,僞私有屬性一塊兒解決了這一問題,僞私有名還阻止了子類偶然地從新定義內部的方法名稱,就像在Sub2中那樣。一樣的,這個功能只對較大型的多人項目有用,並且只用於已選定的變量名。不要將代碼弄得難以置信的混亂。只當單個類真的須要控制某些變量名時,才使用這個功能。對較爲簡單的程序來講,就過頭了。
十一、方法是對象:綁定或無綁定。方法(特別是綁定方法),一般簡化了python中的不少設計目標的實現。在第29章學習__call__的時候簡單的介紹了綁定方法。這裏進行詳細的介紹,而且更通用和靈活。在第19章,介紹了函數能夠和常規對象同樣處理,方法也是一種對象,而且能夠用與其餘對象大部分相同的方式來普遍的使用,能夠對它們賦值,將其傳遞給函數,存儲在數據結構中,等等。因爲類方法能夠從一個實例或一個類訪問,它們實際上在python中有兩種形式:a、無綁定類方法對象:無self。經過對類進行點號運算從而獲取類的函數屬性,會傳回無綁定(unbound)方法對象。調用該方法時,必須明確提供實例對象做爲第一參數。在3.0中,一個無綁定方法和一個簡單的函數是相同的,能夠經過類名來調用;在2.6中,它是一種獨特的類型,而且不提供一個實例就沒法調用;b、綁定實例方法對象:self+函數對。經過對實例進行全運算從而獲取類的函數屬性,會傳回綁定(bound)方法對象。python在綁定方法對象中自動把實例和函數打包,因此,不用傳遞實例去調用該方法。
十二、接上11.這兩種方法都是功能齊全的對象,可四處傳遞,就像字符串和數字。執行時,二者都須要它們在第一參數中的實例(也就是self的值)。這也就是爲何在上一章在子類方法調用超類方法時,要刻意傳入實例。從嚴格意義上來講,這類調用會產生無綁定的方法對象。調用綁定方法對象時,python會自動提供實例,來建立綁定方法對象的實例。也就是說,綁定方法對象一般均可和簡單函數對象互換,並且對於本來就是針對函數而編寫的接口而言,就頗有用了。例如:
如今,在正常操做中,建立了一個實例,在單步中調用了它的方法,從而打印出傳入的參數:
不過,綁定方法對象是在過程當中產生的,就在方法調用的括號前。事實上,咱們能夠獲取綁定方法,而不用實際進行調用。object.name點號運算是一個對象表達式。在下列代碼中,會傳回綁定方法對象,把實例(object1)和方法函數(Spam.doit)打包起來。能夠把這個綁定方法賦值給另外一個變量名,而後像簡單函數那樣進行調用:
另外一方面,若是對類進行點號運算來得到doit,就會獲得無綁定方法對象,也就是函數對象的引用值。要調用這類方法時,必須傳入實例做爲最左側參數:
擴展一下,若是咱們引用的self的屬性是引用類中的函數,那麼相同規則也適用於類的方法。self.method表達式是綁定方法對象,由於self是實例對象:
大多數時候,經過點號運算取出方法後,就是當即調用,因此不會注意到這個過程當中產生的方法對象。可是,若是編寫通用方式調用對象的程序代碼時,就得當心,特別是要注意無綁定方法:無綁定方法通常須要傳入明確的實例對象。
1三、在3.0中,無綁定方法是函數。在3.0中,已經刪除了無綁定方法的概念。咱們在這裏所介紹的無綁定方法,在3.0中看成一個簡單函數對待。對於大多數用途來講,這對於咱們的代碼沒什麼影響;任何一種方式,當經過一個實例來調用一個方法的時候,都會有一個實例傳遞給該方法的第一個參數。顯示類型測試程序可能受到影響,若是打印出一個非實例的類方法,它在2.6中顯示「無綁定方法」(unbound method),在3.0中顯示「函數」(function)。此外在3.0中,不使用一個實例而是調用一個方法是沒有問題的,只要這個方法不期待一個實例,而且經過類調用它而不是經過一個實例調用它。也就是說,只有對經過實例調用,3.0纔會向方法傳遞一個實例,當經過一個類調用的時候,只有在方法期待一個實例的時候,才必須手動傳遞一個實例:
這裏的最後一個測試在2.6中失效,由於無綁定方法默認的須要傳遞一個實例;它在3.0中有效,由於這樣的方法看成一個簡單函數對待,而不須要一個實例。儘管這會刪除3.0中某些潛在的錯誤陷阱(好比忘記傳入實例),但它容許類方法用做簡單的函數,只要它們沒有被傳遞而且不指望一個「self」實例參數。以下的兩個調用仍然在3.0和2.6中都失效了,第一個(經過實例調用)自動把一個實例傳遞給一個並不期待實例的方法,而第二個(經過類調用)不會把一個實例傳遞給確實期待一個實例的方法:
因爲這一修改,對於只經過類名而不經過一個實例調用的、沒有一個self參數的方法,在3.0中再也不須要下一章介紹的staticmethod裝飾器,這樣的方法做爲簡單函數運行,不會接受一個實例參數。在2.6中,這樣的調用是錯誤的,除非手動的傳遞一個實例。
1四、綁定方法和其餘可調用對象:綁定方法能夠做爲一個通用對象處理,就像是簡單函數同樣,它們能夠任意的在一個程序中傳遞。此外,因爲綁定方法在單個的包中組合了函數和實例,所以它們能夠像任何其餘可調用對象同樣對待,而且在調用的時候不須要特殊的語法。例如,以下的例子在一個列表中存儲了4個綁定方法對象,而且隨後使用常規的調用表達式來調用它們:
和簡單函數同樣,綁定方法對象擁有本身的內省信息,包括讓它們配對的實例對象和方法函數訪問的屬性。調用綁定方法會直接分配配對:
實際上,綁定方法只是python中衆多的可調用對象類型中的一種。正以下面說的,簡單函數編寫爲一個def或lambda,實例繼承了一個__call__,而且綁定實例方法都可以以相同的方式對待和調用:
從技術上說,類也屬於可調用對象的範疇,可是,咱們一般調用它們來產生實例而不是作實際的工做,以下:
綁定方法和python的可調用對象模型,一般都是python的設計朝向一種難以置信的靈活語言方向努力的衆多方式中的一些。
1五、爲何要在乎:綁定方法和回調函數:綁定方法會自動讓實例和類方法函數配對,所以能夠在任何但願獲得的簡單函數的地方使用。最多見的使用,就是把方法註冊成tkinter gui接口(2。6中叫作tkinter)中事件回調處理器的代碼。下面是簡單的例子:
要爲按鈕點擊事件註冊一個處理器時,一般是將一個不帶參數的可調用對象傳遞給command關鍵詞參數。函數名(和lambda)均可以使用,而類方法只要是綁定方法也能夠使用:
在這裏,事件處理器是self.handler(一個綁定方法對象),它記住self和MyGui.handler。由於handler稍後因事件而啓用時,self會引用原始實例。所以這個方法能夠讀取在事件間用於保留狀態信息的實例的屬性。若是利用簡單函數,狀態信息通常都必須經過全局變量保存,此外能夠參考29章的__call__運算符重載的討論,來了解另外一種讓類和函數api相容的方式。
1六、多重繼承:「混合」類:不少基於類的設計都要求組合方法的全異的集合,在class語句中,首行括號內能夠列出一個以上的超類。當這麼作時,就是在使用所謂的多重繼承:類和其實例繼承了列出的全部超類的變量名。搜索屬性時,python'會由左至右搜索類首行中的超類,直到找到相符者。從技術上說,任何超類自己可能還有一些其餘的超類,對於更大的類樹,這個搜索能夠更復雜一點:a、在傳統類中(默認的類,直到3.0),屬性搜索處理對全部路徑深度優先,直到繼承樹的頂端,而後從左到右進行;b、在新式類(以及3.0的全部類中),屬性搜索處理沿着樹層級,以更加廣度優先的方式進行。無論哪一種方式,當一個類擁有多個超類的時候,它們會根據class語句頭部行中列出的順序從左到右查找。一般來講,多重繼承是建模屬於一個集合以上的對象的好辦法。例如,一我的能夠是工程師,做家,音樂家等。所以,可繼承這些集合的特性。使用多重繼承,對象得到了全部其超類中行爲的組合。也許多重繼承最多見的用法就是做爲「混合」超類的通用方法。這類超類通常都稱爲混合類:它們提供方法,能夠經過繼承將其加入應用類。例如,python打印類實例對象的默認方式並非很好用。從某種意義上說,混合類相似於模塊:它們提供方法的包,以便在其客戶子類中使用。然而,和模塊中的簡單函數不一樣,混合類中的方法也可以訪問self實例,以使用狀態信息和其餘方法。
1七、編寫混合顯示類:python的默認方式來打印一個類實例對象,並非頗有用:
就像29章學習運算符重載的時候看到的,能夠提供一個__str__或__repr__方法,以實現制定後的字符串表達式形式。可是,若是不在每一個想打印的類中編寫__repr__,爲何不在一個通用工具類中編寫一次,而後在全部類中繼承呢?這就是混合類的用處。在混合類中定義一個顯示方法一次,使得可以在想要看到一個定製顯示格式的任何地方重用它。:a、第27章的AttrDisplay類在一個通用的__str__方法中格式化了實例屬性,可是,它沒有爬升類樹,而且只是用於但集成模式中;b、第28章的classtree.py定義了函數以爬升和遍歷類樹,可是,它沒有顯示對象屬性,而且沒有架構爲一個可繼承類。
1八、這裏在上面的基礎上擴展編碼一組3個混合類,這3個類充當通用的顯示工具,以列出一個類樹上全部對象的實例屬性、繼承屬性和屬性。
1九、接上18;a、用__dict__列出實例屬性。從一個簡單的例子開始--列出附加給一個實例的屬性。以下的類編寫在文件lister.py中,它定義了一個名爲ListInstance的混合類,它對於將其包含到頭部行的全部類都重載了__str__方法。因爲ListInstance編寫爲一個類,因此它成爲了一個通用工具,其格式化邏輯能夠用於任何子類的實例:
ListInstance使用前面介紹的一些技巧來提取實例的類名和屬性:a、每一個實例都有一個內置的__class__屬性,它引用本身所建立自的類;而且每一個類都有一個__name__屬性,它引用了頭部中的名稱,所以,表達式self.__class__.__name__獲取了一個實例的類的名稱;b、這個類經過直接掃描實例的屬性字典(從__dict__中導出),以構建一個字符串來顯示全部實例屬性的名稱和值,從而完成其主要工做。字典的鍵經過排序,以免python跨版本的任何排序差別。這些方面,ListInstance相似於第27章的屬性顯示:實際上,它很大程度上只是一個主題的變體。這裏,咱們的類顯示了兩種其餘技術:a、經過調用id內置函數顯示了實例的內存地址,該函數返回任何對象的地址(根據定義,這是一個惟一的對象標識符,在隨後對這一代碼的修改中有用);b、它針對其同坐方法使用僞私有命名模式:__attrnames。python經過擴展屬性名稱以包含類名,從而把這樣的名稱本地化到其包含類中(在這個例子中,它變成了_ListInstance__attrnames)。對於附加到self的類屬性(如方法)和實例屬性,都是如此。這種行爲在這樣的通用工具中頗有用,由於它確保了其名稱不會與其客戶子類中使用的任何名稱衝突。
20、接19,沒說完的。因爲ListInstance定義了一個__str__運算符重載方法,因此派生自這個類的實例在打印的時候自動顯示其屬性,只給定了比簡單地址多一些的信息。以下是使用這個類,在單繼承模式中(這段代碼在3.0和2.6中同樣工做):
咱們能夠把列表輸出獲取爲一個字符串,而不用str打印出它,而且交互響應仍然使用默認格式:
ListInstance對於咱們所編寫的任何類都是有用的,即使類已經有了一個或多個超類。這就是多繼承的用武之地,經過把ListInstance添加到一個類頭部的超類 列表中(例如,混合進去),咱們能夠仍然繼承本身有超類的同時「自由的」得到__str__。文件testmixin.py:
這裏,Sub從Super和ListInstance繼承了名稱,它是本身的名稱與其超類中名稱的組合。當咱們把生成一個Sub實例並打印它,就會自動得到從ListInstance混合進去的定製表示(在這個例子中,這段腳本的輸出在3.0和2.6下都是相同的,除了對象地址不一樣):
ListInstance在它混入的任何類中都有效,由於self引用拉入了這個類的子類的一個實例,而無論它多是什麼。從某種意義上講,混合類是模塊的類等價形式,它是在各類客戶中有用的方法包。例如,下面是再次在單繼承模式中工做的Lister,它做用於一個不一樣的類實例之上,使用import,而且帶有類以外的屬性設置:
它們除了提供這一工具,還像全部的類同樣,混入了優化代碼維護。例如,若是稍後決定擴展ListInstance的__str__也打印出一個實例繼承的全部類屬性,是安全的;由於它是一個集成的方法,修改__str__自動的更新導入該類和混合該類的每一個子類的顯示。
2一、接19的。使用dir列出繼承的屬性。咱們的Lister混合類只顯示實例屬性(例如,附加到實例對象自身的名稱)。擴展該類以顯示從一個實例能夠訪問的全部屬性,這也是很容易的。這包括它本身以及它所繼承自的類。技巧是使用dir內置函數,而不是掃描實例的__dict__字典,後者只是保存了實例屬性,可是,在2.2及之後的版本中,前者也收集了全部繼承的屬性。以下修改後的代碼實現了這一方案,咱們已經將其從新命名,以便使得測試更簡單,可是,若是用這個替代最初的版本,全部已有的客戶類將自動選擇新的顯示:
注意,這段代碼省略了__X__名稱的值;這些大部分都是內部名稱,咱們一般不會在這樣的通用列表中注意到。這個版本必須使用getattr內置函數來獲取屬性,經過指定字符串而不是使用實例屬性字典索引,getattr使用了繼承搜索協議,而且在這裏列出的一些代碼沒有存儲到實例自身中。要測試新的版本,修改testmixin.py文件並使用新的類來替代:
這個文件的輸出隨着每一個版本而變化。在2.6中,咱們獲得以下輸出。注意,名稱壓縮在lister的方法名中其做用(縮減其所有的值顯示,以節省篇幅):
在3.0中,更多的屬性顯示出來,由於全部的類都是「新式的」,而且從隱式的object超類那裏繼承了名稱(關於object的更多的在31章介紹)。因爲如此多的名稱繼承自默認的超類,咱們已經在這裏省略了不少。自行運行程序以獲得完整的列表:
這裏注意一點,既然咱們也顯示繼承的方法,咱們必須使用__str__而不是__repr__來重載打印。使用__repr__,這段代碼將會循環,顯示一個方法的值,該值觸發了該方法的類的__repr__,從而顯示該類。也就是說,若是lister的__repr__試圖顯示一個方法,顯示該方法的類將再次促發lister的__repr__。在這裏,本身把__str__修改成__repr__來看看。若是你在這樣的環境中使用__repr__,能夠使用isinstance來比較屬性值的類型和標準庫中的types.MethodType,以知道省略哪些項,從而避免循環。
2二、接19。列出類樹中每一個對象的屬性。咱們的lister沒有告訴咱們一個繼承名稱來自哪一個類。然而,正如咱們在第28章末尾的classtree.py示例中看到的,在代碼中爬升類繼承樹很容易。以下的混合類使用這一名稱技術來顯示根據屬性所在的類來分組的屬性,它遍歷了整個類樹,在此過程當中顯示了附加到每一個對象上的屬性。它這樣遍歷繼承樹:從一個實例的__class__到其類,而後遞歸的從類的__bases__到其全部超類,一路掃描對象的__dicts__:
注意,這裏使用一個生成器表達式來導向對超類的遞歸調用,它由嵌套的字符串join方法激活。還要注意,這個版本使用3.0和2.6的字符串格式化方法而不是%來格式化表達式,以使得替代更清晰。當像這樣應用不少替代的時候,明確的參數數目可能使得代碼更容易理解。簡而言之,在這個版本中,咱們把以下的第一行與第二行交換:
如今,修改testmixin.py,再次測試新類繼承:
在2.6中,該文件的樹遍歷輸出以下所示:
注意,在這一輸出中,方法如今在2.6下是無綁定的,由於咱們直接從類獲取它們,而不是從實例。還注意lister的__visited表把本身的名稱壓縮到實際的屬性字典中;除非咱們很不走運,這不會與那裏的其餘數據衝突。在3.0中,咱們再次獲取了額外的屬性和超類。注意,無綁定的方法在3.0中是簡單的函數,正如本章前面說的(再次刪除了對象中大多數內置對象以節省篇幅,能夠自行運行這段代碼以獲取完整的列表):
這個版本經過保留一個目前已經訪問過的類的表來避免兩次列出一樣的類對象(這就是爲何一個對象的id包含其中,以充當一個以前顯示項的鍵)。和第24章的過渡性模塊重載程序同樣,字典在這裏用來避免重複和循環,由於類對象多是字典鍵。集合也能夠提供相似的功能。這個版本還會再次經過省略__X__名稱來避免較大的內部對象。若是註釋掉這些名稱的測試,它們的值將會正常顯示。這是在2.6下輸出的摘要,帶有這一臨時性的修改(整個輸出很大,而且在3.0中這種狀況甚至變得更糟,所以,這些名字可能會有所忽略):
爲了更有趣,嘗試把這個類混合到更實質的某些內容中,例如python的thinter gui工具箱模塊的button類。一般,想要在一個類的頭部命名ListTree(最左端),所以,它的__str__會被選取:Button也有一個,而且在多繼承中最左端的超類首先搜索。以下的輸出十分龐大(18K個字符),所以,本身運行這段代碼看看完整的列表(而且,若是在使用2.6,記住應該對模塊名使用Tkinter 而不是tkinter):
ps:支持slot:因爲它們掃描示例詞典,因此這裏介紹的ListInstance和ListTree類不能直接支持存儲在slot中的屬性--slot是一種新的、相對不多使用的選項,在下一章中,示例屬性將在一個__slots__類屬性中聲明。例如,若是在textmixin.py中,咱們在Super中賦值__slots__=['data1'],在Sub中賦值__slots__ = ['data3'],只有data2屬性經過這兩個lister類顯示在該實例中:ListTree也會顯示data1和data3,可是是做爲Super和Sub類對象的屬性,而且是它們的值的一種特殊格式(從技術上說,它們都是類級別的描述符)。要更好的支持這些類中的slot屬性,把__dict__掃描循環修改成使用下一章給出的代碼來迭代__slots__列表,而且使用getattr內置函數來獲取值,而不是使用__dict__索引(ListTree已經這麼作了)。既然實例只繼承最低的類的__slots__,當__slots__列表出如今多個超類中的時候,能夠提出一種策略(ListTree已經將它們顯示爲類屬性)。ListInherited對全部這些都是免疫的,由於dir結果組合了__dict__名稱和全部類的__slots__名稱。並且能夠直接容許代碼處理基於slot的屬性(就像當前所作的那樣),而不是將其複雜化爲一種少用的、高級的特性。slot和常規的實例屬性是不一樣的名稱,在下一章介紹slot。
2三、類是對象:通用對象的工廠。有時候,基於類的設計要求要建立的對象來響應條件,而這些條件是在編寫程序的時候沒法預料的。工廠設計模式容許這樣的一種延遲方法。在很大程度上因爲python的靈活性,工廠能夠採起多種形式,其中的一些根本不會顯得特殊。類是對象,能夠把類傳給會產生任意種類對象的函數,這類函數在oop設計 領域中偶爾稱爲工廠。這些函數是cpp這類強類型語言的主要工做,可是在python中很容易實現,第17章介紹的apply函數和更新的替代語法,能夠用一步調用帶有任意構造方法參數的類,從而產生任意種類的實例(這種語法能夠調用任何可調用的對象,包括函數、類和方法。這裏的factory函數也會運行任何可調用的對象,而不只僅是類(儘管參數名稱是這樣)。此外,正如咱們在18章看到的,2.6有一種aClass(*args)的替代方法:apply(aClass,args)內置調用,這個在3.0中已經刪除了):
這段代碼中,定義了一個對象生成器函數,稱爲factory。它預期傳入的是類對象(任何對象都行),還有該類構造函數的一個或多個參數。這個函數使用特殊的「varargs」調用語法來調用該函數並返回實例。例子的其他部分只是定義了兩個類,並將其傳給factory函數以產生二者的實例。而這就是在python中編寫的工廠函數所須要作的事。它適用於任何類以及任何構造函數參數。可能的改進是,在構造函數調用中支持關鍵詞參數。工廠函數可以經過**args參數收集參數,並在類調用中傳遞它們:
在python中一切都是「對象」,包括類(類在cpp中僅僅是編譯器的輸入而已)。只有從類衍生的對象纔是python中的oop對象。
2四、爲何有工廠。回想下第25章以抽象方式介紹的例子processor,以及本章再次做爲「has-a」關係的組合例子。這個程序接受讀取器和寫入器對象來處理任意的數據流。這個例子的原始版本能夠手動傳入特定的類的實例,例如,FileWriter和SocketReader,來調整正被處理的數據流。後面會傳入硬編碼的文件、流以及格式對象。在更爲動態的場合下,像配置文件或gui這類外部工具可能用來配置流。在這種動態世界中,可能沒法在腳本中把流的接口對象的創建方式固定的編寫好。可是有可能根據配置文件的內容在運行期間建立它。例如,這個文件可能會提供從模塊導入的流的類的字符串名稱,以及選用構造函數的調用參數。工廠式的函數或程序代碼在這裏可能很方便,由於它們可讓咱們取出並傳入沒有預先在程序中硬編碼的類。實際上,這些類在編寫程序時可能白不存在:
這裏,getattr內置函數依然用於取出特定字符串名稱的模塊屬性(很像obj.attr,但attr是字符串)。由於這個程序代碼片斷是假設的單獨的構造函數參數,所以並不見的須要factory或apply:咱們可以使用aclass(classarg)直接建立其實例。然而,存在未知的參數列表時,它們就可能有用了,而通用的工廠編碼模式能夠改進代碼的靈活性。
2五、與設計相關的其餘話題:本章中介紹了繼承、複合、委託、多繼承、綁定方法和工廠,這些是在python程序中組合類的全部經常使用模式。在設計模式領域,這些是冰山一角。本書的其餘部分的索引:a、抽象超類(第28章);b、裝飾器(第31章和第38章);c、類型子類(第31章);d、靜態方法和類方法(第31章);e、管理屬性(第37章);f、元類(第31章和第39章)。
7、類的高級主體(31章)
本部分將介紹一些與類相關的高級主題,做爲第6部分的結束:研究如何創建內置類型的子類、新式類的變化和擴展、靜態方法和類方法、函數裝飾器等。建議讀者可以從事或者研究較大的python oop項目,做爲本書的補充。
一、擴展內置類型。除了實現新的種類的對象之外,類也會用擴展python的內置類型的功能,從而支持更另類的數據結構。例如,要爲列表增長隊列插入和刪除方法,能夠寫些類,包裝(嵌入)列表對象,而後導出可以以特殊方式處理該列表的插入和刪除的方法,就像30章的委託技術。
二、接1.經過嵌入擴展類型。在16章和18章所寫的那些集合函數,下面是它們以python類的形式重生的樣子。下面的例子(setwrapper.py)把一些集合函數變成方法,並且新增了一些基本運算符重載,實現了新的集合對象。對於多數類而言,這個類只是包裝了python列表,以及附加的集合運算。由於這是類,因此也支持多個實例和子類繼承的定製。和咱們前面的函數不一樣,這裏使用類容許咱們建立多個自包含的集合對象,帶有預先設置的數據和行爲,而不是手動把列表傳入函數中:
要使用這個類,咱們生成實例、調用方法,而且像往常同樣運行定義的運算符:
重載索引運算讓Set類的實例能夠充當真正的列表。
三、經過子類擴展類型。全部內置類型如今都能直接建立子類。像list、str、dict以及tuple這些類型轉換函數都變成內置類型的名稱:雖然腳本看不見,但類型轉換調用(例如,list(‘spam’))其實啓用了類型的對象構造函數。這樣的改變能夠經過用戶定義的class語句,定製或擴展內置類型的行爲:創建類型名稱的子類並對其進行定製。類型的子類實例,可用在原始的內置類型可以出現的任何地方。例如,假設對python列表偏移值以0開始計算而不是1開始一直很困擾,咱們也能夠本身編寫本身的子類,定製列表的核心行爲。文件typesubclass.py說明了如何去作:
在這個文件中,MyList子類擴展了內置list類型的__getitem__索引運算方法,把索引1 到N映射爲實際的0到N-1.它所作的其實就是把提交的索引值減1,以後繼續調用超類版本的索引運算,可是這樣,就足夠了:
此輸出包括打印類索引運算的過程。像這樣改變索引運算是不是好事是另外一回事:MyList類的使用者,對於這種和python序列行爲有所偏離的困惑程度可能也都不一樣。通常來講,用這種方式定製內置類型,能夠說是很強大的。例如:這樣的編碼模式會產生編寫集合的另外一種方式:做爲內置list類型的子類,而不是管理內嵌列表對象的獨立類、正如第5章說的,python帶有一個強大的內置集合對象,還有常量和解析語法能夠生成新的集合。然而,本身編寫一個集合,一般仍然是學習類型子集創建過程的一種好方法。下面的setsubclass.py文件內,經過定製list來增長和集合處理相關的方法和運算符。由於其餘全部行爲都是從內置list超類繼承而來的,這樣能夠獲得較短和較簡單的替代作法:
下面是文件末尾測試代碼的輸出。由於建立核心類型的子類是高級功能,這裏省略其餘的細節:
python中還有更高效的方式,也就是經過字典實現集合:把這裏的集合實現中的線性掃描換成字典索引運算(散列),所以運行時會快不少。
四、新式類。在2.2中,引入一種新的類,稱爲「新式」(new-style)類。本教程這一部分至今爲止所談到的類和新的類相比時,就稱爲「經典」(classic)類。在3.0中,類的區分已經融合了,可是對於2.X的用戶來講,仍是有所區別的:a、對於3.0來講,全部的類都是咱們所謂的「新式類」,無論它們是否顯式的繼承自object。全部的類都繼承自object,無論是顯式的仍是隱式的,而且,全部的對象都是object的實例;b、在2.6及其之前的版本中,類必須繼承自的類看做是「新式」object(或者其餘的內置類型),而且得到全部新式類的特性。也就是當3.0中全部的類都是自動是新式類,因此新式類的特性只是常規的類的特性。然而,在本節中,這裏選擇進行區分,以便對2.X代碼的用戶有所區分,這些代碼中的類,只有在它們派生自object的時候才具備新式類的特性。在2.6及其以前的版本中,惟一的編碼差別是,它們要麼從一個內置類型(如list)派生,要麼從一個叫作object的特殊內置類派生。若是沒有其餘合適的內置類型可用,內置名稱object就能夠做爲新式類的超類提供:
一般狀況下,任何從object或其餘內置類型派生的類,都會自動視爲新式類。只要一個內置類型位於超類樹中的某個位置,新類也看成一個新式類。不是從內置類型派生出來的類,就會看成經典類來對待。新式類只是和經典類有細微的差異,而且它們之間的區分的方式,對於大多數主要的python用戶來講,是可有可無的,並且,經典類形式在2.6中仍然可用,而且與以前幾乎徹底同樣的工做。實際上,新式類在語法和行爲上幾乎與經典類徹底向後兼容;它們主要只是添加了一些高級的新特性。然而,因爲它們修改了一些類行爲,它們必須做爲一種不一樣的工具引入,以免影響到依賴之前的行爲的任何已有代碼。例如,一些細微的區別,例如鑽石模式繼承搜索和帶有__getattr__這樣的管理屬性方法的內置運算,若是保持不變的話,可能會致使一些遺留代碼失效。
五、新式類變化。新式類在幾個方面不一樣於經典類,其中一些是很細微的,:a、類和類型合併,類如今就是類型,而且類型如今就是類。實際上,這兩者基本上同義詞。type(I)內置函數返回一個實例所建立自的類,而不是一個通用的實例類型,而且,一般是和 I.__class__相同的。此外,類是type類的實例,type可能子類話爲定製類建立,而且全部的類(以及由此全部的類型)繼承自object。;b、繼承搜索順序,多繼承的鑽石模式有一種略微不一樣的搜索順序,整體而言,它們可能先橫向搜索在縱向搜索,而且先寬度優先搜索,再深度優先搜索;c、針對內置函數的屬性獲取。__getattr__和__getattribute__方法再也不針對內置運算的隱式屬性獲取而運行。這意味着,它們再也不針對__X__運算符重載方法名而調用,這樣的名稱搜索從類開始,而不是從實例開始;d、新的高級工具。新式類有一組新的類工具,包括slot、特性、描述符和__getattribute__方法。這些工具中的大多數都有很是特定的工具構建目的。在第27章的邊欄部分簡單的介紹了這些變化的3個,而且,將在第37章的屬性管理介紹中以及第38章的私有性裝飾器介紹中更深刻的回顧它們。這裏的a和b可能影響到已有的2.X代碼,在介紹新式類以前,更詳細的看看這些工具。
六、類型模式變化。在新式類中,類型和類的區別已經徹底消失了。類自身就是類型:type對象產生類做爲本身的實例,而且類產生它們的類型的實例。實際上,像列表和字符串這樣的內置類型和編寫爲類的用戶定義類型之間沒有真正的區別。這就是爲何咱們能夠子類化內置類型,就像本章前面介紹的那樣,因爲子類化一個列表這樣的內置類型,會把一個類變爲新式的,所以,它變成了一個用戶定義的類型。除了容許子類化內置類型,還有一點變得很是明顯的狀況,就是當咱們進行顯式類型測試的時候。使用2.6的經典類,一個類實例的類型是一個通用的「實例」,可是,內置對象的類型要更加特定:
可是,對於2.6中的新式類,一個類實例的類型是它所建立自的類,由於類直接是用戶定義的類型,實例的類型是它的類,而且,用戶定義的類的類型與一個內置對象類型的類型相同。類如今有一個__class__屬性,由於它們也是type的實例:
對於3.0中的全部類都是如此,由於全部的類自動都是新式的,即使它們沒有顯式的超類。實際上,內置類型和用戶定義類型之間的區分,在3.0中消失了:
正如看到的,在3.0中,類就是類型,可是,類型也是類。從技術上說,每一個類都是一個元類生成,元類是這樣的一個類,它要麼是type自身,要麼是它定製來擴展或管理生成的類的一個子類。除了影響到進行類型測試的代碼,這對於工具開發者來講,是一個重要的鉤子。
七、類型測試的隱含意義:除了提供內置類型定製和元類鉤子,新的類模式中類和類型的融合,可能會影響到進行類型測試的代碼。例如:在3.0中,類實例的類型直接而有意義的比較,而且以與內置類型對象一樣的方式進行。下面的代碼基於這樣一個事實:類如今是類型,而且一個實例的類型是該實例的類:
對於2.6或更早版本中的經典類,比較實例類型幾乎是無用的,由於全部的實例都具備相同的「實例」類型。要真正的比較類型,必需要比較實例__class__屬性(若是你關注可移植性,這在3.0中也有效,但在那裏不是必需的):
而且,正如所期待的,在這方面,2.6中的新式類與3.0中的全部類一樣的工做,比較實例類型會自動的比較實例的類:
固然,類型檢查一般在python程序中是錯誤的事情(咱們編寫對象接口,而不是編寫對象類型),而且更加通用的isinstance內置函數極可能是在極少數狀況下(即必須查詢實例類的類型的狀況下)想要使用的。
八、全部對象派生自object。新式類模式中的另外一個類型變化是,因爲全部的類隱式的或顯式的派生自(繼承自)類object,而且,因爲全部的類型如今都是類,因此每一個對象都派生自object內置類,無論是直接的或經過一個超類。考慮3.0中的以下交互模式(在2.6中編寫一個顯式的object超類,會有等價的效果):
和前面同樣,一個類實例的類型就是它所產生自的類,而且,一個類的類型就是type類,由於類和類型都融合了。確實,可是實例和類都派生自內置的object類,所以,每一個類都有一個顯式或隱式的超類:
對於列表和字符串這樣的內置類型來講,也是如此,由於在新模式中,類型就是類,內置類型如今也是類,而且他們的實例也派生自object:
實際上,類型自身派生自object,而且object派生自type,即使兩者是不一樣的對象,一個循環的關係覆蓋了對象模型,而且由此致使這樣一個事實:類型是生成類的類:
實際上,這種模式致使了比前面的經典類的類型/類區分的幾個特殊狀況,而且,它容許咱們編寫假設並使用一個object超類的代碼。
九、鑽石繼承變更。也許新式類中最顯著的變化就是,對於所謂的多重繼承樹的鑽石模型(diamond pattern)的繼承(也就是有一個以上的超類會通往同一更高的超類)處理方式有點不一樣。鑽石模式是高級設計概念,在python編程中不多用到,而且在本書中目前爲止尚未討論過,因此這裏不深究。簡單來講,對經典類而言,繼承搜索程序是絕對深度優先,而後纔是由左至右。python一路往上搜索,深刻樹的左側,返回後,纔開始找右側。在新式類中,在這類狀況下,搜索相對來講是寬度優先的。python先尋找第一個搜索的右側的全部超類,而後才一路往上搜索至頂端共同的超類,換句話說,搜索過程先水平進行,而後向上移動。搜索算法也比這裏介紹的複雜一些,不過了解這些就夠了。由於有這樣的變更,較低超類能夠重載較高超類的屬性,不管它們混入的是哪一種多重繼承樹。此外,當從多個子類訪問超類的時候,新式搜索規則避免重複訪問同一超類。
十、鑽石繼承例子。爲了說明起見,舉一個經典類構成的簡單鑽石繼承模式的例子,這裏D是B和C的超類,B和C都導向相同的祖先A:
此外是在超類A中內找到屬性的。由於對經典類來講,繼承搜索是先往上搜索到最高,而後返回再往右搜索:python會先搜索D、B、A,而後纔是C(可是,當attr在A找到時,B之上的就會中止)。這裏,對於派生自object這樣的內置類的新式類,以及3.0中的全部類,搜索順序是不一樣的:python會先搜索C(B的右側),而後纔是A(B之上):也就是先搜索D、B、C,而後纔是A(在這個例子中,則會停在C處):
這種繼承搜索流程的變化是基於這樣的假設:若是在樹中較低處混入C,和A相比,可能會比較想獲取C的屬性。此外,這也是假設C老是要覆蓋A的屬性:當C獨立使用時,多是真的,可是當C混入經典類鑽石模式時,可能就不是這樣了。當編寫C時,可能根本不知道C會以這樣的方式混入。在這個例子中,可能程序員認爲C應該覆蓋A,儘管如此,新式類先訪問C。不然,C將會在鑽石環境中基本無心義:它不會定製A,而且只對同名的C使用。
十一、明確解決衝突。固然,假設的問題就是這是假設的。若是難以記住這種搜索順序的偏好,或者若是你想對搜索流程有更多的控制,均可以在樹中任何地方強迫屬性的選擇:經過賦值或者在類混合處指出你想要的變量名:
在這裏,經典類樹模擬了新式類的搜索順序:在D中爲屬性賦值,使其挑選C中的版本,於是改變了正常的繼承搜索路徑(D.attr位於樹中最低的位置)。新式類也能選擇類混合處以上的屬性來模擬經典類:
若是願意以這樣的方式解決這種衝突,大體上就能忽略搜索順序的差別,而不依賴假設來決定所編寫的類的意義,天然,以這種方式挑選的屬性也能夠是方法函數(方法是正常可賦值的對象):
在這裏,明確在樹中較低處賦值變量名以選取方法。咱們也能夠明確調用所須要的類。在實際應用中,這種模式可能更爲經常使用,尤爲是構造函數:
這類在混合點進行賦值運算或調用而作的選擇,能夠有效的把代碼從類的差別性中隔離出。經過這種方式明確的解決衝突,能夠確保你的代碼不會因之後更新的python版本而有所變化(除了2.6中,新式類須要從object或內置類型派生類以使用新式工具以外)。ps:即便沒有經典/新式類的差別,這種技術在通常多重繼承場合中也很方便。若是想要左側超類的一部分以及右側超類的一部分,可能就須要在子類中明確使用賦值語句,告訴python要選擇哪一個同名屬性。此外,鑽石繼承模式在有些狀況下的問題,比此處所提到的還要多(例如,若是B和C都有所需的構造函數會調用A中的構造器,那該怎麼辦?),因爲這樣的語境在python中很罕見,再也不本書範圍內。
十二、搜索順序變化的範圍。總之,默認狀況下,鑽石模式對於經典類和新式類進行不一樣的搜索,而且這是一個非向後兼容的變化。此外要記住,這種變化主要影響到多繼承的鑽石模式狀況。新式類繼承對於大多數其餘的繼承樹結構都是不變的工做。此外,整個問題不可能在理論上比實踐中更重要,由於,新式類搜索直到2.2才足夠顯著的解決,而且在3.0中才成爲標準,它不可能影響到太多的python代碼。正如已經提到的,即使沒有在本身編寫的類中用到鑽石模式,因爲隱式的object超類在3.0中的每一個類之上,因此現在多繼承的每一個例子都展現了鑽石模式。也就是說,在新式類中,object自動扮演了前面討論的實例中類A的角色。所以,新的類搜索規則不只修改了邏輯語義,並且經過避免屢次訪問相同的類而優化了性能。一樣,新模式的隱式object超類爲各類內置操做提供了默認方法,包括__str__和__repr__顯示格式化方法。運行一個dir(object)來看看提供哪些方法。沒有一個新式的搜索順序,在多繼承狀況中,object中的默認方法將老是覆蓋用戶編寫的類中的從新定義,除非這些重定義老是放在最左邊的超類之中。換句話說,新類模式自身使得使用新搜索順序更關鍵!
1三、新式類的擴展。除了鑽石繼承搜索模式中的這個改變之外(過於罕見,能夠看看就行),新式類還啓用了一些更爲高級的可能性。下面將對這些額外特性中的每個給出概覽,這些特性在2.6的新式類和3.0的全部類中均可用。
1四、slots實例。將字符串屬性名稱順序賦給特殊的__slots__類屬性,新式類就有可能既限制類的實例將有的合法屬性集,又可以優化內存和速度性能。這個特殊屬性通常是在class語句頂層內將字符串名稱順序賦給變量__slots__而設置:只有__slots__列表內的這些變量名可賦值爲實例屬性。然而,就像python中的全部變量名,實例屬性名必須在引用前賦值,即便是列在__slots__中也是這樣。如下是說明的例子:
slot對於python的動態特性來講是一種違背,而動態特性要求任何名稱均可以經過賦值來建立。這個功能看做是捕捉"打字錯誤"的方式(對於再也不__slots__內的非法屬性名作賦值運算,就會偵測出來),並且也是最優化機制。若是建立了不少實例而且只有幾個屬性是必須的話,那麼爲每一個實例對象分配一個命名空間字典可能在內存方面代價過於昂貴。要節省空間和執行速度,slot屬性能夠順序存儲以供快速查找,而不是爲每一個實例分配一個字典。
1五、slot和通用代碼。實際上,有些帶有slots的實例也許根本沒有__dict__屬性字典,使得有些書中縮寫的源程序過於複雜(包括本書中一些代碼)。工具根據字符串名稱通用的列出屬性或訪問屬性,例如,必須當心使用比__dict__更爲存儲中立的工具,例如getattr、setattr和dir內置函數,它們根據__dict__或__slots__存儲應用於屬性。在某些狀況下,兩種屬性源代碼都須要查詢以確保完整性。例如,使用slots的時候,實例一般沒有一個屬性字典,python使用第37章介紹的類描述符功能來爲實例中給的slots屬性分配空間。只有slot列表中的名稱能夠分配給實例,可是,基於 slot的屬性仍然能夠使用通用工具經過名稱來訪問或設置。在3.0中,(以及在2,6中派生自object的類中):
沒有一個屬性命名空間字典,不可能給不是slots列表中名稱的實例來分配新的名稱:
然而,經過在__slots__中包含__dict__仍然能夠容納額外的屬性,從而考慮到一個屬性空間字典的需求。在這個例子中,兩種存儲機智都用到了,可是getattr這樣的通用工具容許咱們將它們看成單獨一組屬性對待:
然而,想要通用的列出全部實例屬性的代碼,可能仍然須要考慮兩種存儲形式,由於dir也返回繼承的屬性(這依賴於字典迭代器來收集鍵):
因爲兩種均可能忽略,更正確的編碼方式以下所示(getattr考慮到默認狀況):
1六、超類中的多個__slot__列表。注意,上面這段代碼只是解決了由一個實例繼承的最低__slots__屬性中的slot名稱。若是類樹中的多個類都有本身的__slots__屬性,通用的程序必須針對列出的屬性開發其餘的策略(例如,把slot名稱劃分爲類的屬性,而不是實例的屬性)。slot聲明可能出如今一個類樹中的多個類中,可是,它們受到一些限制,除非理解slot做爲類級別描述符的實現,不然要說明這些限制有些困難:a、若是一個子類繼承自一個沒有__slots__的超類,那麼超類的__dict__屬性老是能夠訪問的,使得子類中的一個__slots__無心義;b、若是一個類定義了與超類相同的slot名稱,超類slot定義的名稱版本只有經過直接從超類獲取其描述符才能訪問;c、因爲一個__slots__聲明的含義受到它出現其中的類的限制,因此子類將有一個__dict__,除非它們也定義了一個__slots__;d、一般從列出實例屬性這方面來講,多類中的slots可能須要手動類樹爬升,dir用法,或者把slot名稱看成不一樣的名稱領域的政策:
當這種通用性可能的時候,slots可能最好看成類屬性來對待,而不是視圖讓它們表現出與常規類屬性同樣。
1七、類特性。有一種稱爲特性(property)的機制,提供另外一種方式讓新式類定義自動調用的方法,來讀取或賦值實例屬性。這種功能是第29章說過,目前用的不少的__getattr__和__setattr__重載方法的替代作法。特性和這兩個方法有相似效果,可是隻在讀取所須要的動態計算的變量名時,纔會發生額外的方法調用。特性(和slots)都是基於屬性描述器(attribute descriptor)的新概念(這裏不說)。簡單來講,特性是一種對象,賦值給類屬性名稱。特性的產生是以三種方法(得到、設置以及刪除運算的處理器)以及經過文檔字符串調用內置函數property。若是任何參數以None傳遞或省略,該運算就不能支持。特性通常都是在class語句頂層賦值【例如,name = property(...)】。這樣賦值時,對類屬性自己的讀取(例如,obj.name)。就會自動傳給property的一個讀取方法。例如,__getattr_-方法可以讓類攔截未定義屬性的引用:
下面是相同的例子,改用特性來編寫。(注意,特性對於全部類可用,可是,對於攔截屬性賦值,必須是2.6中object派生的新式對象纔有效):
就某些編碼任務而言,特性比起傳統技術不是那麼複雜,並且運行起來更快。例如,當咱們新增屬性賦值運算支持時,特性就變得更有吸引力:輸入的代碼更少,對咱們不但願動態計算的屬性進行賦值運算時,不會發生額外的方法調用:
等效的經典類可能會引起額外的方法調用,並且須要經過屬性字典傳遞屬性賦值語句,以免死循環(或者,對於新式類,會導向object超類的__setattr__):
就這個簡單的例子而言,特性彷佛是贏家。然而,__getattr__和__setattr__的某些應用依然須要更爲動態或通用的接口,超出特性所能直接提供的範圍。例如,在大多數狀況下,當類編寫時,要支持的屬性集沒法確認,並且甚至沒法以任何具體形式存在(例如,委託任意方法的引用給被包裝/嵌入對象時).在這種狀況下,通用的__getattr__或__setattr__屬性處理器外加傳入的屬性名,會是更好的選擇。由於這類通用處理器也能處理較簡單的狀況,特性大體上就只是選用的擴展功能了。
1八、__getattribute__和描述符。__getattribute__方法只適用於新式類,可讓類攔截全部屬性的引用,而不侷限於未定義的引用(如同__getattr__)。可是,它遠比__getattr__和__setattr__難用,並且很像__setattr__多用於循環,但兩者的用法不一樣。除了特性和運算符重載方法,python支持屬性描述符的概念,帶有__get__和__set__方法的類,分配給類屬性而且由實例繼承,這攔截了對特定屬性的讀取和寫入訪問。描述符在某種意義上是特性的一種更加通用的形式。實際上,特性是定義特性類型描述符的一種簡化方式,該描述符運行關於訪問的函數。描述符還用來實現前面介紹的slots特性。因爲特性、__getattribute__和描述符都是較爲高級,放在後面說。
1九、元類。新式類的大多數變化和功能增長,都是前面提到的可子類化的類型的概念密切相連,由於在2.2及其之後的版本中,可子類化的類型和新式類與類型和類的合併一塊兒引入。正如看到的,在3.0中,合併完成了:類如今是類型,而且類型也是類。除了這些變化,python還針對編寫元類增長了一種更加一致的協議,元類是子類化了type對象而且攔截類建立調用的類。此外,它們還爲管理和擴展類對象提供了一種定義良好的鉤子。
20、靜態方法和類方法。在2.2中,有可能在類中定義兩種方法,它們不用一個實例就能夠調用:靜態方法大體與一個類中的簡單的無實例函數相似的工做,類方法傳遞一個類而不是一個實例。儘管這一功能與前面小節所介紹的新式類一塊兒添加,靜態方法和類方法只對經典類有效。要使用這種方法,必須在類中調用特殊的內置函數,分別名爲staticmethod和classmethod,或者使用後面的裝飾語法來調用。在3.0中,無實例的方法只經過一個類名來調用,而不須要一個staticmethod聲明,可是這樣的方法卻實經過實例來調用。
2一、爲何使用特殊方法。類方法一般在其第一個參數中傳遞一個實例對象,以充當方法調用的一個隱式主體。然而今天,有兩種方法來修改這種模式。有時候,程序須要處理與類而不是與實例相關的數據。考慮要記錄由一個類建立的實例的數目,或者維護當前內存中一個類的全部實例的列表。這種類型的信息及其處理與類相關,而非與其實例相關。也就是說,這種信息一般存儲在類自身上,不須要任何實例也能夠處理。對於這樣的任務,一個類以外的簡單函數編碼每每就能勝任,由於它們能夠經過類名訪問類屬性,它們可以訪問類數據而且不須要經過一個實例。然而,要更好的把這樣的代碼與一個類聯繫起來,而且容許這樣的過程像一般同樣用繼承來定製,在類自身之中編寫這類函數將會更好。因此,須要一個類中的方法不只不傳遞並且也不期待一個self實例參數。python經過靜態方法的概念來支持這樣的目標,嵌套在一個類中的沒有self參數的簡單函數,而且旨在操做類屬性而不是實例屬性。靜態方法不會接受一個自動的self參數,無論是經過一個類仍是一個實例調用。它們一般會記錄跨全部實例的信息,而不是爲實例提供行爲。python還支持類方法的概念,這是類的一種方法。傳遞給它們的第一個參數是一個類對象而不是一個實例,無論是經過一個實例或一個類調用它們。即使是經過一個實例調用,這樣的方法也能夠經過它們的self類參數來訪問類數據。常規的方法(如今正規的方法叫作實例方法)在調用的時候仍然接受一個主題實例,靜態方法和類方法則不會。
2二、2.6和3.0中的靜態方法。靜態方法概念在2.6和3.0中是相同的,可是實現需求在3.0中有所發展。還記得2.6和3.0老是給經過一個實例調用的方法傳遞一個實例。然而,3.0對待從一個類直接獲取的方法,與2.6有所不一樣:a、在2.6中,從一個類獲取一個方法會產生一個未綁定方法,沒有手動傳遞一個實例的就不會調用它;b、在3.0中,從一個類獲取一個方法會產生一個簡單函數,沒有給出實例也能夠常規的調用。也就是在2.6類方法老是要求傳入一個實例,無論是經過一個實例或類調用它們。相反,在3.0中,只有當一個方法期待實例的時候,咱們纔給它傳入一個實例,沒有一個self實例參數的方法能夠經過類調用而不須要傳入一個實例。也就是說,3.0容許類中的簡單函數,只要它們不期待而且也不傳入一個實例參數。直接效果是:a、在2.6中,必須老是把一個方法聲明爲靜態的,從而不帶一個實例而調用它,無論是經過一個類或一個實例調用它;b、在3.0中,若是方法只經過一個類調用的話,不須要將這樣的方法聲明爲靜態的,可是,要經過一個實例調用它,必須這麼作。
2三、接22。例如,假設想使用類屬性去計算從一個類產生了多少實例。下面的文件spam.py作出了最初的嘗試,它的類把一個計數器存儲爲類屬性,每次建立一個新的實例的時候,構造函數都會對計數器加1,而且,有一個顯示計數器值的方法。記住,類屬性是由全部實例共享的,因此能夠把計數器放在類對象內,從而確保它能夠在全部的實例中使用:
printNumInstances方法旨在處理類數據而不是實例數據,它是關於全部實例的,而不是某個特定的實例。所以,想要沒必要傳遞一個實例就能夠調用它。實際上,不想生成一個實例來獲取實例的數目,由於這可能會改變咱們想要獲取的實例的數目!換句話說,咱們想要一個無self的"靜態"方法。然而,這段代碼是否有效,取決於咱們所使用的python,以及咱們調用方法的方式,經過類或者經過一個實例。在2.6中(以及更一般的2.X),經過類和實例調用無self方法函數都將失效:
這裏的問題在於,2.6中無綁定實例的方法並不徹底等同於簡單函數,即便在def頭部沒有參數,該方法在調用的時候仍然期待一個實例,由於該函數與一個類相關。在3.0中(以及隨後的3.X中),對一個無self方法的調用使得經過類調用有效,但從實例調用失效:
也就是說,對於printNumInstances這樣的無實例方法的調用,在2.6中經過類進行調用將會失效,可是在3.0中有效。另外一方面,經過一個實例調用在兩個版本中的python都會失效,由於一個實例自動傳遞給方法,而該方法沒有一個參數來接收它:
若是可以使用3.0而且堅持只經過類調用無self方法,就已經有了一個靜態方法特性。然而,要容許非self方法在2.6中經過類調用,而且在2.6和3.0中都經過實例調用,須要採起其餘設計,或者可以把這樣的方法標記爲特殊的。
2四、靜態方法替代方案。若是不能使得一個無self方法稱爲特殊的,有一些不一樣的編碼結構能夠嘗試。若是想要調用沒有一個實例二訪問類成員的函數,可能最簡單的就是隻在類以外生成他們的簡單函數,而不是類方法。經過這種方式,調用中不會期待一個實例。例如,對spam.py的以下修改在3.0和2.6中都有效:
由於類名稱對簡單函數而言是可讀取的全局變量,這樣可正常工做。此外,函數名變成了全局變量,這僅適用於這個單一的模塊而已。它不會和程序其餘文件中的變量名衝突。在python中的靜態方法以前,這一結構是通用的方法。因爲python已經把模塊提供爲命名空間分隔工具,所以能夠肯定一般不須要把函數包裝到一個類中,除非它們實現了對象行爲。像這裏這樣的模塊中的簡單函數,作了無實例類方法的大多數工做,而且已經與類關聯起來,由於他們位於同一個模塊中。不過這個方法仍然不是理想的,首先,它給該文件的做用域添加了一個額外的名稱,該名稱只用來處理單個的類。此外,該函數與類的直接關聯很小;實際上,它的定義可能在數百行代碼以外的位置。可能更糟糕的是,像這樣的簡單函數不能經過繼承定製,由此,他們位於類的命名空間以外:子類不能經過從新定義這樣的一個函數來直接替代或擴展它。咱們有可能像要像一般那樣使用一個常規方法並老是經過一個實例調用它,從而使得這個例子以獨立於版本的方式工做:
惋惜,正如前面說的,若是沒有一個實例可用,而且產生一個實例來改變類數據,就像這裏的最後一行所說明的那樣,這樣的方法徹底是沒法工做的。更好的解決方法多是在類中把一個方法標記爲不須要一個實例。
2五、使用靜態和類方法。還有一個選擇就是編寫和類相關聯的簡單函數。在2.2中,能夠用靜態和類方法編寫類,二者都不須要在啓用時傳入實例參數。要設計這個類的方法時,類要調用內置函數staticmethod和classmethod,就像以前討論過的新式類中提到的那樣。它們都把一個函數標記爲特殊的,例如,若是是靜態方法的話不須要實例,若是是一個類方法的話須要一個類參數,例如:
ps:程序代碼中最後兩個賦值語句只是從新賦值方法名稱smeth和cmeth罷了。在class語句中,經過賦值語句進行屬性的創建和修改,因此這些最後的賦值語句會覆蓋稍早由def所做的賦值。從技術上說,python如今支持三種類相關的方法:實例、靜態和類。此外,3.0也容許類中的簡單函數在經過一個類調用的時候充當靜態方法的角色而不須要額外的協議,從而擴展這一模式。實例方法是本教程中所見的常規(默認)狀況。必定要用實例對象調用實例方法,經過實例調用時,python會把實例自動傳給第一個(最左側)參數。類調用時,須要手動傳入實例:
反之,靜態方法調用時不須要實力參數。與類以外的簡單函數不一樣,其變量名位於定義所在類的範圍內,屬於局部變量,並且能夠經過繼承查找。非實例函數一般在3.0中能夠經過類調用,可是在2.6中並不是默認的。使用staticmethod內置方法容許這樣的方法在3.0中經過一個實例調用,而在2.6中經過類和實例調用(前者在3.0中沒有staticmethod也能工做,可是後者不行):
類方法相似,當python自動把類(而不是實例)傳入類方法第一個(最左側)參數中,無論它是經過一個類或一個實例調用:
2六、使用靜態方法統計實例。如今有了這些內置函數,下面是本節實例統計示例的靜態方法等價形式,把方法標記爲特殊的,以便不會自動傳遞一個實例:
使用靜態方法內置函數,咱們代碼如今容許在2.6和3.0中經過類或其任何實例來調用無self方法:
和將printNumInstances移到類以外的作法相比較,這個版本還須要額外的staticmethod調用,然而,這樣作把函數名稱變成類做用域內的局部變量(不會和模塊內的其餘變量名衝突),並且把函數程序代碼移動靠近其使用的地方(位於class語句中),而且容許子類用集成定製靜態方法,這是比超類編碼中從文件導入函數更方便的一種方法,下面是子類以及新的測試會話:
此外,類能夠繼承靜態方法而不用從新定義它,它能夠沒有一個實例而運行,無論定義於類樹的什麼地方:
2七、用類方法統計實例。類方法也能夠作上述相似的工做,下面的代碼與前面列出的靜態方法版本具備相同的行爲,可是,它使用一個類方法來把實例的類接收到其第一個參數中。類方法使用通用的自動傳遞類對象,而不是硬編碼類名稱:
這個類與前面的版本使用方法相同,可是經過類和實例調用printNumInstances方法的時候,它接受類而不是實例:
當使用類方法的時候,他們接收調用的主題的最具體(低層)的類。當試圖經過傳入類更新類數據的時候,這具備某些細微的隱藏含義。例如,若是在模塊test.py中像前面那樣對定製子類化,擴展spam.printNumInstances以顯示其cls參數,而且開始一個新的測試會話:
不管什麼時候運行一個類方法的時候,最低層的類傳入,即使對於沒有本身的類方法的子類:
這裏的第一個調用中,經過Sub子類的一個實例調用了一個類方法,而且python傳遞了最低的類,sub,給該類方法。在這個例子中,因爲該方法的sub重定義顯式的調用了spam超類的版本,spam中的超類方法在第一個參數中接收本身。可是,對於直接繼承類方法的一個對象。看看發生了什麼:
這裏的最後一個調用把other傳遞給了spam的類方法。在這個例子中有效的,由於它經過繼承獲取了在spam中找到的計數器。若是該方法試圖把傳遞的類的數據賦值,它將更新object,而不是spam。在這個特定的例子中,可能spam經過直接編寫本身的類名來更新其數據會更好,而不是依賴於傳入的類參數。
2八、使用類方法統計每一個類的實例。因爲類方法老是接收一個實例樹中的最低類:a、靜態方法和顯式類名稱可能對於處理一個類本地的數據來講是更好的解決方案;b、類方法可能更適合處理對層級中的每一個類不一樣的數據。代碼須要管理每一個類實例計數器,這可能會更好的利用類方法。在下面的代碼中,頂層的超類使用一個類方法來管理狀態信息,該信息根據樹中的每一個類都不一樣,並且存儲在類上,這相似於實例方法管理類實例中狀態信息的方式:
靜態方法和類方法都有其餘高級的做用,在最近的python中,隨着裝飾器語法的出現,靜態方法和類方法的設計都變得更加簡單,這一語法容許在2.6和3.0中擴展類,已初始化最後一個示例中numInstances這樣的計數器的數據。
30、裝飾器和元類:第一部分。上一節的staticmethod調用技術,對於一些人來講比較奇怪,因此新增的函數裝飾器(function decorator)讓這個運算變得簡單一點,提供一個方式,替函數明確了特定的運算模式,也就是將函數包裹了另外一層,在另外一函數的邏輯內實現。函數裝飾器變成了通用的工具:除了靜態方法用法外,也可用於新增多種邏輯的函數。例如,能夠用來記錄函數調用的信息和在出錯時檢查傳入的參數類型等。從某種程度上來講,函數裝飾器相似第30章的委託設計模式,可是其設計是爲了加強特定的函數或方法調用,而不是整個對象接口。python提供一些內置函數裝飾器,來作一些運算,例如,標識靜態方法,可是能夠編寫本身的任意裝飾器。雖然不限於使用類,但用戶定義的函數裝飾器一般也寫成類,把原始函數和其餘數據當成狀態信息。在2.6和3.0中也有更新的相關擴展可用:類裝飾器直接綁定到類模式,而且它們的用途與元類有所重疊。
3一、函數裝飾器基礎。從語法上說,函數裝飾器是它後邊的函數的運行時的聲明。函數裝飾器是寫成一行,就在定義函數或方法的def語句以前,並且由@符號、後面跟着所謂的元函數(metafunction)組成:也就是管理另外一個函數(或其餘可調用對象)的函數。例如,現在的靜態方法能夠用下面的裝飾器語法編寫:
從內部來看,這個語法和下面的寫法有相同效果(把函數傳遞給裝飾器,再賦值給最初的變量名):
結果就是,調用方法函數的名稱,其實是觸發了它staticmethod裝飾器的結果。由於裝飾器會傳回任何種類的對象,這也可讓裝飾器在每次調用上增長一層邏輯。裝飾器函數可返回原始函數,或者新對象(保存傳給裝飾器的原始函數,這個函數將會在額外邏輯層執行後間接的運行)。通過這些添加,有了在2.6和3.0中編寫前一節中的靜態方法示例的一種更好的方法(classmethod裝飾器以一樣的方式使用):
記得,staticmethod仍然是一個內置函數;它能夠用於裝飾語法中,只是由於它把一個函數看成參數而且返回一個可調用對象。實際上,任何這樣的函數均可以以這種方式使用,即使是下節介紹的本身編寫的用戶定義函數。
3二、裝飾器例子。儘管python提供了不少內置函數,它們能夠用做裝飾器,咱們也能夠本身編寫定製裝飾器。因爲普遍應用,(在本教程最後一個部分中介紹,但是在我寫的博文中有可能不會出現),這裏做爲一個快速的示例,看看一個簡單的用戶定義的裝飾器的應用。想一想地29章,__call__運算符重載方法爲類實例實現函數調用接口。下面的程序經過這種方法定義類,在實例中存儲裝飾的函數,並捕捉對最初變量名的調用。由於這是類,也有狀態信息(記錄所做調用的計數器):
由於spam函數是經過tracer裝飾器執行的,因此當最初的變量名spam調用時,實際上觸發的是類中的__call__方法。這個方法會計算和記錄改次調用,而後委託給原始的包裹的函數。注意*name參數語法是如何打包並解開傳入的參數的。所以,裝飾器可用於包裹攜帶任意數目參數的任何函數。結果就是新增一層邏輯至原始的spam函數,下面是此腳本的輸出:第一列來自tracer類,第二列來自spam函數:
這個裝飾器對於任何接收位置參數的函數都有效,可是,它沒有返回已裝飾函數的結果,沒有處理關鍵字參數,而且不能裝飾類方法函數(簡單來講,對於其__call__方法將只傳遞一個tracer實例)。第八部分有各類各樣的方式來編寫函數裝飾器,包括嵌套def語句;其中的一些替代方法比這裏給出的更適合於方法。
3三、類裝飾器和元類。函數裝飾器如此有用,以致於2.6和3.0都擴展了這一模式,容許裝飾器應用於類和函數,簡單來講,裝飾器相似於函數裝飾器,可是,它們在一條class語句的末尾運行,而且把一個類名從新綁定到一個可調用對象。一樣,它們能夠用來管理類(在類建立以後),或者當隨後建立實例的時候插入一個包裝邏輯層來管理實例。代碼以下:
被映射爲下列至關代碼:
類裝飾器也能夠擴展類自身,或者返回一個攔截了隨後的實例構建調用的對象。例如,在本章前面的「用類方法統計每一個類的實例」小節的示例中,咱們使用這個鉤子來自動的擴展了帶有實例計數器和任何其餘所需數據的類:
元類是一種相似的基於類的高級工具,其用途每每與類裝飾器有所重合。它們提供了一種可選的模式,會把一個類對象的建立導向到頂級type類的一個子類,在一條class語句的最後:
在2.6中,效果是相同的,可是編碼是不一樣的,在類頭部中使用一個類屬性而不是一個關鍵字參數:
元類一般從新定義type類的__new__或__init__方法,以實現對一個新的類對象的建立和初始化的控制。直接效果就像類裝飾器同樣,是定義了在類建立時自動運行的代碼。兩種方法均可以用來擴展一個類或返回一個任意的對象來替代它,幾乎是擁有無限的、基於類的可能性的一種協議。
3四、類陷阱。大多數類的問題一般均可以濃縮爲命名空間的問題(由於類只是多了一些技巧的命名空間而已)。修改類屬性的反作用。從理論上來講,類(和類實例)是可改變的對象。就像內置列表和字典同樣,能夠給類屬性賦值,而且進行在原處的修改,同時意味着修改類或實例對象,也會影響對它的多處 引用。這一般是咱們想要的(也是對象通常修改其狀態的方式),修改類屬性時,瞭解這點很重要,由於全部從類產生的實例都共享這個類的命名空間,任何在類層次所做的修改都會反映在全部實例中,除非實例擁有本身的被修改的類屬性版本。由於類、模板以及實例都只是屬性命名空間內的對象,通常可經過賦值語句在運行時修改它們的屬性。在類主體中,對變量名a的賦值語句會產生屬性X.a,在運行時存在於類的對象內,並且會由全部X的實例繼承:
到目前爲止,都不錯,可是,當咱們在class語句外動態修改類屬性時,將發生什麼:這也會修改每一個對象從該類繼承而來的這個屬性。再者,在這個進程或程序執行時,由類所建立的新實例會獲得這個動態設置值,不管該類的源代碼是什麼:
這個功能是好的仍是壞的你?在第26章學過,能夠修改類的屬性而不修改實例,就能夠達到相同的目的。這種技術能夠模擬其餘語言的「記錄」或「結構體」。考慮下面不常見可是合法的python程序:
在這裏,類X和Y就像「無文件」模塊:存儲不想發生衝突的變量的命名空間。這是徹底合法的python程序設計技巧,可是使用其餘人編寫的類就不合適了。永遠沒法知道,修改的類屬性會不會對類內部行爲產生重要影響。若是要仿真C的結構體,最好是修改實例而不是類,這樣的話,影響的只有一個對象:
3五、修改可變的類屬性也可能產生反作用。這個陷阱實際上是前面的陷阱的擴展。因爲類屬性由全部實例共享,因此若是一個類屬性引用一個可變對象,那麼從任何實例來原處修改該對象都會馬上影響到全部實例:
這個效果與以前的效果沒區別:可變對象經過簡單變量來共享,全局變量由函數共享,模塊級的對象由多個導入者共享,可變的函數參數由調用者和被調用者共享。全部這些都是通用行爲的例子,而且若是從任何引用原處修改共享的對象的話,對一個可變對象的多個引用都將受到影響。在這裏,這經過繼承發生於全部實例所共享的類屬性中,可是,這也是一樣的現象在發揮做用。經過對實例屬性自身的賦值的不一樣行爲,這可能會更含蓄的發生:
可是,這不是一個問題,它只是須要注意的事情:共享的可變類屬性在python程序中可能有不少有效的用途。
3六、多重繼承:順序很重要。若是使用多重繼承,超類別再class語句首行內的順序就很重要。python老是會更加超類在首行的順序,由左至右搜索超類。例如,在第30章多重繼承的例子中,假設super類也實現了__str__方法:
咱們想要繼承Lister的仍是SUper的呢,因爲繼承搜索從左到右,會從先列在sub類首行的那個類取得該方法,假設,先編寫ListTree,由於這個類的整個目的就是其定製了的__str__(實際上,當把這個類與擁有本身的一個__str__的tkinter,Button混入的時候,必須這麼作)。但如今,假設Super和ListTree各自有其餘的同名屬性的版本。若是想要使用Super的變量名,也想要使用ListTree的變量名,在類首行的編寫順序就沒什麼幫助:就得手動對Sub類內的屬性名賦值來覆蓋繼承:
在這裏,對sub類中other作賦值運算,會創建sub.ohter,對super.other對象的引用值。因它在樹中的位置較低,sub.other實際上會隱藏ListTree.other(繼承搜索時)。一樣,若是在類首行中先編寫super來挑選其中other,就須要刻意的選ListTree中的方法:
多重繼承是高級工具,即便掌握了上一段的內容,謹小慎微的使用依然是必須的。不然對於任意關係較遠的子類中變量的含義,將會取決於混入的類的順序。經驗是:當混合類儘量的獨立完備時,多重繼承的工做情況最好,由於混合類能夠應用在各類環境中,所以不該該對樹中其餘類相關的變量名有任何假設。以前第30章的僞私有__X屬性功能能夠把類依賴的變量名本地化,限制混合類能夠混入的名稱,所以會有幫助,例如,在這個例子中,若是ListTree只是要導出特殊的__str__,就能夠將其另外一個方法命名爲__other,從而避免發生與其餘類衝突。
3七、類、方法以及嵌套做用域,這個陷阱在2.2引入嵌套函數做用域後就消失了,這裏做爲回顧下,由於這能夠示範當一層嵌套是類時,新的嵌套函數做用域會發什麼什麼。類引入本地做用域,就像函數同樣。因此相同的做用域行爲也會發生在class語句的主體中。此外,方法是嵌套函數,也有相同的問題。當類進行嵌套時,看起來使人困惑就比較常見了。下面的例子(nester.py),generate函數返回嵌套的spam類的實例。在其代碼中,類名稱spam是在generate函數的本地做用域中賦值的。可是,在2.2以前,在類的方法函數中,是看不見類名稱spam的。方法只能讀取其本身的本地做用域,generate所在的模塊以及內置變量名:
這個例子可在2.2之後的版本中執行,由於任何所在函數def的本地做用域都會自動被嵌套的def中看見。可是,2.2以前的版本就行不通了。注意,即便在2.2中,方法def仍是沒法看見所在類的局部做用域。方法def只看得見所在def的局部做用域。這就是爲何方法得經過self實例,或類名稱取引用所在類語句中定義的方法和其餘屬性。例如,方法中的程序代碼必須使用self.count或spam.count,不能只是count。若是正在使用2.2版之前的版本,有不少方式能夠使用上一個例子,最簡單的就是,全局聲明,把spam放在所在模塊的做用域中。由於方法看得見所在模塊中的全局變量名,就可以引用spam:
事實上,這種作法適用於全部python版本。通常而言,若是避免嵌套類和函數,代碼都會比較簡單。若是想複雜一些,能夠徹底放棄在方法中引用spam,而是用特殊的__class__屬性,來返回實例的類對象:
3八、python中基於委託的類:__getattr__和內置函數。在第27章的類教程和第30章的委託介紹中簡單的遇到這個問題:使用__getattr__運算符重載方法來把屬性獲取委託給包裝的對象的類,在3.0中將失效,除非運算符重載方法在包裝類中從新定義了。在3.0(2.6中,當使用新式類的時候),內置操做沒有導向到通用的屬性攔截方法,從而隱式的獲取運算符重載方法的名稱,例如,打印所使用的__str__方法,不會調用__getattr__。相反,3.0在類中查找這樣的名字,而且徹底略過常規的運行時實例查找機制。爲了解決這一點,這樣的方法必須在包裝類中重定義,要麼手動,要麼使用工具,或者在超累中從新定義。