Python 内编写类的各种技巧和方法
簡(jiǎn)介
有關(guān) Python 內(nèi)編寫類的各種技巧和方法(構(gòu)建和初始化、重載操作符、類描述、屬性訪問(wèn)控制、自定義序列、反射機(jī)制、可調(diào)用對(duì)象、上下文管理、構(gòu)建描述符對(duì)象、Pickling)。 你可以把它當(dāng)作一個(gè)教程,進(jìn)階,或者使用參考;我希望它能夠成為一份針對(duì) Python 方法的用戶友好指南。
內(nèi)容目錄
介紹 構(gòu)建和初始化 使操作符在自定義類內(nèi)工作 神奇方法——比較 神奇方法——數(shù)字 描述你的類 屬性訪問(wèn)控制 制作自定義序列 反射 可調(diào)用對(duì)象 上下文管理 構(gòu)建描述符對(duì)象 Pickling 你的對(duì)象總結(jié) 附錄:如何調(diào)用神奇方法1.介紹
這份指南是幾個(gè)月內(nèi)最有價(jià)值的 Blog 投稿精華。它的主題是向大家講述 Python 中的神奇方法。
何為神奇方法呢?它們是面向 Python 中的一切,是一些特殊的方法允許在自己的定義類中定義增加“神奇”的功能。它們總是使用雙下劃線(比如 __ init__ 或 __ lt__),但它們的文檔沒有很好地把它們表現(xiàn)出來(lái)。所有這些神奇方法都出現(xiàn)在Python的官方文檔中,但內(nèi)容相對(duì)分散,組織結(jié)構(gòu)也顯得松散。還有你會(huì)難以發(fā)現(xiàn)一個(gè)實(shí)例(雖然他們被設(shè)計(jì)很棒,在語(yǔ)言參考中被詳細(xì)描述,可之后就會(huì)伴隨著枯燥的語(yǔ)法描述等)。
所以,為了解決我認(rèn)為在 Python 文檔中的一大敗筆,我打算用更多純英語(yǔ),實(shí)例驅(qū)動(dòng)的文檔來(lái)說(shuō)明 Python 的神奇方法。然后我就開始花了幾周的時(shí)間來(lái)寫 blog,而現(xiàn)在我已經(jīng)完成了它們,并將它們合訂成一份指南。
我希望你喜歡它。把它當(dāng)作一個(gè)教程,進(jìn)階,或者使用參考;我希望它能夠成為一份針對(duì) Python 方法的用戶友好指南。
2.構(gòu)建和初始化
相信大家都熟悉這個(gè)最基礎(chǔ)的神奇方法 __ init__。它令你能自定義一個(gè)對(duì)象的初始化行為。而當(dāng)我調(diào)用x=SomeClass() 時(shí),__ init__ 并不是最先被調(diào)用的。實(shí)際上有一個(gè)叫做 __ new__ 的方法,事實(shí)上是它創(chuàng)建了實(shí)例,它傳遞任何參數(shù)給初始化程序來(lái)達(dá)到創(chuàng)建的目的。在對(duì)象生命周期結(jié)束時(shí),調(diào)用 __ del__。讓我們更近地觀察下這 3 個(gè)神奇方法吧:
- __ new__(cls,[…)
一個(gè)對(duì)象的實(shí)例化時(shí) __ new__ 是第一個(gè)被調(diào)用的方法。在類中傳遞其他任何參數(shù)到 __ init__。__new__很少被使用,這樣做確實(shí)有其目的,特別是當(dāng)一個(gè)子類繼承一個(gè)不可改變的類型(一個(gè)元組或一個(gè)字符串)時(shí)。我不打算再繼續(xù)深入追求 __ new __ 的細(xì)節(jié)了,因?yàn)檫@不會(huì)產(chǎn)生多大用處,因?yàn)樵?Python Docs 內(nèi)已經(jīng)涵蓋了一份巨詳細(xì)的說(shuō)明了。
- __ init__(self,[…)
類的初始化。它會(huì)獲得初始構(gòu)建調(diào)用傳過(guò)來(lái)的任何東西(舉例來(lái)說(shuō)就是,當(dāng)我們調(diào)用x=SomeClass(10,‘foo’),__ init__ 就會(huì)把傳過(guò)來(lái)的 10 和 ‘foo’ 作為參數(shù)。__init__在 Python 的類定義中幾乎普遍被使用)
- __ del__(self)
如果 __ new__和 __ init__ 是對(duì)象的構(gòu)造器,那么 __ del__ 就是析構(gòu)器。它不實(shí)現(xiàn)聲明為del x(這樣的代碼不會(huì)解釋成 x.__ del__())的行為。相反,它定義為當(dāng)一個(gè)對(duì)象被垃圾回收時(shí)的行為。這可能對(duì)可能需要額外清理的對(duì)象相當(dāng)有用,比如 sockets 或文件對(duì)象。但要小心,如果對(duì)象仍處于存活狀態(tài)而當(dāng)被解釋退出時(shí),__ del__ 沒有保證就會(huì)被執(zhí)行,因此這樣的__ del__ 不能作為良好的編碼規(guī)范的替代。(就像當(dāng)你完成操作總是要關(guān)閉一次連接。但事實(shí)上,__ del__ 幾乎永遠(yuǎn)不會(huì)執(zhí)行,就因?yàn)樗幱诓话踩闆r被調(diào)用了。使用時(shí)保持警惕!)
把上述這些內(nèi)容合在一起,就成了一份 __ init__ 和 __ del__ 的實(shí)際使用用例:
''' 遇到問(wèn)題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:857662006 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' from os.path import join class FileObject: '''對(duì)文件對(duì)象的包裝,確保文件在關(guān)閉時(shí)得到刪除'''def __init__(self, filepath='~', filename='sample.txt'): # 按filepath,讀寫模式打開名為filename的文件 self.file=open(join(filepath,filename), 'r+')def __del__(self): self.file.close() del self.file3.使操作符在自定義類內(nèi)工作
使用 Python 神奇方法的優(yōu)勢(shì)之一就是它提供了一種簡(jiǎn)單的方式能讓對(duì)象的行為像內(nèi)建類型。這意味著你可以避免用丑陋,反直覺和非標(biāo)準(zhǔn)方法執(zhí)行基本運(yùn)算。在某些語(yǔ)言中,通常會(huì)這樣做:
if instance.equals(other_instance): # do something你也應(yīng)該在 Python 確實(shí)會(huì)這樣做,但同時(shí)它會(huì)增加用戶的疑惑以及不必要的冗長(zhǎng)。不同的庫(kù)可能會(huì)對(duì)相同的運(yùn)算采用不同的命名,這使得用戶比平常干了更多的事。依靠神奇方法的力量,你可以定義一個(gè)方法(比如 __ eq__),然后帶代替我們真實(shí)的意圖:
if instance == other_instance: # do something現(xiàn)在你看到的是神奇方法力量的一部分。絕大多數(shù)都允許我們定義為運(yùn)算符本身的意義,當(dāng)用在我們自己定義的類上就像它們是內(nèi)建類型。
3.1 神奇方法——比較
Python 有一整套神奇方法被設(shè)計(jì)用來(lái)通過(guò)操作符實(shí)現(xiàn)對(duì)象間直觀的比較,而非別扭的方法調(diào)用。它們同樣提供了一套覆蓋 Python 對(duì)象比較的默認(rèn)行為(通過(guò)引用)。以下是這些方法的列表以及做法:
__ cmp__(self, other)
__ cmp__是神奇方法中最基礎(chǔ)的一個(gè)。實(shí)際上它實(shí)現(xiàn)所有比較操作符行為(<,==,!=,等),但它有可能不按你想要的方法工作(例如,一個(gè)實(shí)例是否等于另一個(gè)這取決于比較的準(zhǔn)則,以及一個(gè)實(shí)例是否大于其他的這也取決于其他的準(zhǔn)則)。如果 self < other,那 __ cmp__ 應(yīng)當(dāng)返回一個(gè)負(fù)整數(shù);如果 self == other,則返回 0;如果 self > other,則返回正整數(shù)。它通常是最好的定義,而不需要你一次就全定義好它們,但當(dāng)你需要用類似的準(zhǔn)則進(jìn)行所有的比較時(shí),__ cmp__ 會(huì)是一個(gè)很好的方式,幫你節(jié)省重復(fù)性和提高明確度。
__ eq__(self, other)定義了相等操作符,==的行為。__ ne__(self, other)定義了不相等操作符,!= 的行為。__ lt__(self, other)定義了小于操作符,< 的行為。__ gt__(self, other)定義了大于操作符,> 的行為。__ le__(self, other)定義了小于等于操作符,<=的行為。__ ge__(self, other)定義了大于等于操作符,>= 的行為。舉一個(gè)例子,設(shè)想對(duì)單詞進(jìn)行類定義。我們可能希望能夠按內(nèi)部對(duì) string 的默認(rèn)比較行為,即字典序(通過(guò)字母)來(lái)比較單詞,也希望能夠基于某些其他的準(zhǔn)則,像是長(zhǎng)度或音節(jié)數(shù)。在本例中,我們通過(guò)單詞長(zhǎng)度排序,以下給出實(shí)現(xiàn):
class Word(str): '''單詞類,比較定義是基于單詞長(zhǎng)度的'''def __new__(cls, word): # 注意,我們使用了__new__,這是因?yàn)閟tr是一個(gè)不可變類型, # 所以我們必須更早地初始化它(在創(chuàng)建時(shí)) if ' ' in word: print "單詞內(nèi)含有空格,截?cái)嗟降谝徊糠?#34; word = word[:word.index(' ')] # 在出現(xiàn)第一個(gè)空格之前全是字符了現(xiàn)在 return str.__new__(cls, word)def __gt__(self, other): return len(self) > len(other) def __lt__(self, other): return len(self) < len(other) def __ge__(self, other): return len(self) >= len(other) def __le__(self, other): return len(self) <= len(other)現(xiàn)在,我們可以創(chuàng)建 2 個(gè)單詞(通過(guò) Word(‘foo’) 和 Word(‘bar’))并基于它們的長(zhǎng)度進(jìn)行比較了。注意,我們沒有定義 __ eq__ 和 __ ne__。這是因?yàn)檫@可能導(dǎo)致某些怪異的行為(特別是當(dāng)比較 Word(‘foo’) == Word(‘bar’) 將會(huì)得到 True 的結(jié)果)?;趩卧~長(zhǎng)度的相等比較會(huì)令人摸不清頭腦,因此我們就沿用了str 本身的相等比較的實(shí)現(xiàn)。
現(xiàn)在可能是一個(gè)好時(shí)機(jī)來(lái)提醒你一下,你不必重載每一個(gè)比較相關(guān)的神奇方法來(lái)獲得各種比較。標(biāo)準(zhǔn)庫(kù)已經(jīng)友好地為我們?cè)谀0?functools 中提供了一個(gè)裝飾(decorator)類,定義了所有比較方法。你可以只重載 __ eq__ 和一個(gè)其他的方法(比如 __ gt__,__ lt__,等)。這個(gè)特性只在 Python2.7(后?)適用,但當(dāng)你有機(jī)會(huì)的話應(yīng)該嘗試一下,它會(huì)為你省下大量的時(shí)間和麻煩。你可以通過(guò)在你自己的重載方法在加上 @total_ordering 來(lái)使用。
3.2 神奇方法——數(shù)字
就像你可以通過(guò)重載比較操作符的途徑來(lái)創(chuàng)建你自己的類實(shí)例,你同樣可以重載數(shù)字操作符。系好你們的安全帶,朋友們,還有很多呢。處于本文組織的需要,我會(huì)把數(shù)字的神奇方法分割成5塊:一元操作符,常規(guī)算術(shù)操作符,反射算術(shù)操作符,增量賦值,類型轉(zhuǎn)換。
一元操作符
一元運(yùn)算和函數(shù)僅有一個(gè)操作數(shù),比如負(fù)數(shù),絕對(duì)值等
__ pos__(self)實(shí)現(xiàn)一元正數(shù)的行為(如:+some_object)__ neg__(self)實(shí)現(xiàn)負(fù)數(shù)的行為(如: -some_object)__ abs__(self)實(shí)現(xiàn)內(nèi)建 abs() 函數(shù)的行為__ invert__(self)實(shí)現(xiàn)用~操作符進(jìn)行的取反行為。你可以參考 Wiki:bitwise operations 來(lái)解釋這個(gè)運(yùn)算符究竟會(huì)干什么 常規(guī)算術(shù)操作符現(xiàn)在我們涵蓋了基本的二元運(yùn)算符:+,-,* 等等。其中大部分都是不言自明的。__ add__(self, other)實(shí)現(xiàn)加法__ sub__(self, other)實(shí)現(xiàn)減法__ mul__(self, other)實(shí)現(xiàn)乘法__ floordiv__(self, other)實(shí)現(xiàn)地板除法,使用 // 操作符__ div__(self, other)實(shí)現(xiàn)傳統(tǒng)除法,使用 / 操作符__ truediv__(self, other)實(shí)現(xiàn)真正除法。注意,只有當(dāng)你 from __ future__ import division 時(shí)才會(huì)有效__ mod__(self, other)實(shí)現(xiàn)求模,使用 % 操作符__ divmod__(self, other)實(shí)現(xiàn)內(nèi)建函數(shù) divmod() 的行為__ pow__(self, other)實(shí)現(xiàn)乘方,使用 ** 操作符__ lshift__(self, other)實(shí)現(xiàn)左按位位移,使用 << 操作符__ rshift__(self, other)實(shí)現(xiàn)右按位位移,使用 >> 操作符__ and__(self, other)實(shí)現(xiàn)按位與,使用 & 操作符__ or__(self, other)實(shí)現(xiàn)按位或,使用 | 操作符__ xor__(self, other)實(shí)現(xiàn)按位異或,使用 ^ 操作符反射算術(shù)操作符
你知道我會(huì)如何解釋反射算術(shù)操作符?你們中的有些人或許會(huì)覺得它很大,很可怕,是國(guó)外的概念。但它實(shí)際上很簡(jiǎn)單,下面給一個(gè)例子:
some_object + other
這是“常規(guī)的”加法。而反射其實(shí)相當(dāng)于一回事,除了操作數(shù)改變了改變下位置:
other + some_object
因此,所有這些神奇的方法會(huì)做同樣的事等價(jià)于常規(guī)算術(shù)操作符,除了改變操作數(shù)的位置關(guān)系,比如第一個(gè)操作數(shù)和自身作為第二個(gè)。此外沒有其他的操作方式。在大多數(shù)情況下,反射算術(shù)操作的結(jié)果等價(jià)于常規(guī)算術(shù)操作,所以你盡可以在剛重載完 __ radd__就調(diào)用 __ add__。干脆痛快:
__radd__(self, other)實(shí)現(xiàn)反射加法__rsub__(self, other)實(shí)現(xiàn)反射減法__rmul__(self, other)實(shí)現(xiàn)反射乘法__rfloordiv__(self, other)實(shí)現(xiàn)反射地板除,用 // 操作符__rdiv__(self, other)實(shí)現(xiàn)傳統(tǒng)除法,用 / 操作符__rturediv__(self, other)實(shí)現(xiàn)真實(shí)除法,注意,只有當(dāng)你 from __future__ import division 時(shí)才會(huì)有效__rmod__(self, other)實(shí)現(xiàn)反射求模,用 % 操作符__rdivmod__(self, other)實(shí)現(xiàn)內(nèi)置函數(shù) divmod() 的長(zhǎng)除行為,當(dāng)調(diào)用 divmod(other,self) 時(shí)被調(diào)用__rpow__(self, other)實(shí)現(xiàn)反射乘方,用 ** 操作符__rlshift__(self, other)實(shí)現(xiàn)反射的左按位位移,使用 << 操作符__rrshift__(self, other)實(shí)現(xiàn)反射的右按位位移,使用 >> 操作符__rand__(self, other)實(shí)現(xiàn)反射的按位與,使用 & 操作符__ror__(self, other)實(shí)現(xiàn)反射的按位或,使用 | 操作符__rxor__(self, other)實(shí)現(xiàn)反射的按位異或,使用 ^ 操作符增量賦值
Python 也有各種各樣的神奇方法允許用戶自定義增量賦值行為。你可能已經(jīng)熟悉增量賦值,它結(jié)合了“常規(guī)的”操作符和賦值。如果你仍不明白我在說(shuō)什么,下面有一個(gè)例子:
x = 5 x += 1 # 等價(jià) x = x + 1這些方法都不會(huì)有返回值,因?yàn)橘x值在 Python 中不會(huì)有任何返回值。反而它們只是改變類的狀態(tài)。列表如下:
__iadd__(self, other)實(shí)現(xiàn)加法和賦值__isub__(self, other)實(shí)現(xiàn)減法和賦值__imul__(self, other)實(shí)現(xiàn)乘法和賦值__ifloordiv__(self, other)實(shí)現(xiàn)地板除和賦值,用 //= 操作符__idiv__(self, other)實(shí)現(xiàn)傳統(tǒng)除法和賦值,用 /= 操作符__iturediv__(self, other)實(shí)現(xiàn)真實(shí)除法和賦值,注意,只有當(dāng)你 from __future__ import division 時(shí)才會(huì)有效__imod__(self, other)實(shí)現(xiàn)求模和賦值,用 %= 操作符__ipow__(self, other)實(shí)現(xiàn)乘方和賦值,用 **= 操作符__ilshift__(self, other)實(shí)現(xiàn)左按位位移和賦值,使用 <<= 操作符__irshift__(self, other)實(shí)現(xiàn)右按位位移和賦值,使用 >>= 操作符__iand__(self, other)實(shí)現(xiàn)按位與和賦值,使用 &= 操作符__ior__(self, other)實(shí)現(xiàn)按位或和賦值,使用 |= 操作符__ixor__(self, other)實(shí)現(xiàn)按位異或和賦值,使用 ^= 操作符類型轉(zhuǎn)換的神奇方法
Python 也有一組神奇方法被設(shè)計(jì)用來(lái)實(shí)現(xiàn)內(nèi)置類型轉(zhuǎn)換函數(shù)的行為,如 float()
__int__(self)實(shí)現(xiàn)到 int 的類型轉(zhuǎn)換__long__(self)實(shí)現(xiàn)到 long 的類型轉(zhuǎn)換__float__(self)實(shí)現(xiàn)到 float 的類型轉(zhuǎn)換__complex__(self)實(shí)現(xiàn)到復(fù)數(shù)的類型轉(zhuǎn)換__oct__(self)實(shí)現(xiàn)到 8 進(jìn)制的類型轉(zhuǎn)換__hex__(self)實(shí)現(xiàn)到 16 進(jìn)制的類型轉(zhuǎn)換__index__(self)實(shí)現(xiàn)一個(gè)當(dāng)對(duì)象被切片到 int 的類型轉(zhuǎn)換。如果你自定義了一個(gè)數(shù)值類型,考慮到它可能被切片,所以你應(yīng)該重載__index____trunc__(self)當(dāng) math.trunc(self) 被調(diào)用時(shí)調(diào)用。__trunc__ 應(yīng)當(dāng)返回一個(gè)整型的截?cái)?#xff0c;(通常是 long)__coerce__(self, other)該方法用來(lái)實(shí)現(xiàn)混合模式的算術(shù)。如果類型轉(zhuǎn)換不可能那 __coerce__ 應(yīng)當(dāng)返回 None。 否則,它應(yīng)當(dāng)返回一對(duì)包含 self 和 other(2 元組),且調(diào)整到具有相同的類型4.描述你的類
用一個(gè)字符串來(lái)說(shuō)明一個(gè)類這通常是有用的。 在 Python 中提供了一些方法讓你可以在你自己的類中自定義內(nèi)建函數(shù)返回你的類行為的描述。
__str__(self)當(dāng)你定義的類中一個(gè)實(shí)例調(diào)用了 str(),用于給它定義行為__repr__(self)當(dāng)你定義的類中一個(gè)實(shí)例調(diào)用了 repr(),用于給它定義行為。 str() 和 repr() 主要的區(qū)別在于它的閱讀對(duì)象。 repr() 產(chǎn)生的輸出主要為計(jì)算機(jī)可讀(在很多情況下,這甚至可能是一些有效的 Python 代碼),而 str() 則是為了讓人類可讀。__unicode__(self)當(dāng)你定義的類中一個(gè)實(shí)例調(diào)用了 unicode(),用于給它定義行為。 unicode() 像是 str(),只不過(guò)它返回一個(gè) unicode 字符串。 警惕!如果用戶用你的類中的一個(gè)實(shí)例調(diào)用了 str(),而你僅定義了 __unicode__(),那它是不會(huì)工作的。 以防萬(wàn)一,你應(yīng)當(dāng)總是定義好 __str__(),哪怕用戶不會(huì)使用 unicode__hash__(self)當(dāng)你定義的類中一個(gè)實(shí)例調(diào)用了 hash(),用于給它定義行為。 它必須返回一個(gè)整型,而且它的結(jié)果是用于來(lái)在字典中作為快速鍵比對(duì)。__nonzero__(self)當(dāng)你定義的類中一個(gè)實(shí)例調(diào)用了 bool(),用于給它定義行為。 返回 True 或 False,取決于你是否考慮一個(gè)實(shí)例是 True 或 False 的。我們已經(jīng)相當(dāng)漂亮地干完了神奇方法無(wú)聊的部分(無(wú)示例),至此我們已經(jīng)討論了一些基礎(chǔ)的神奇方法,是時(shí)候讓我們向高級(jí)話題移動(dòng)了。
5.屬性訪問(wèn)控制
有許多從其他語(yǔ)言陣營(yíng)轉(zhuǎn)到 Python 來(lái)的人抱怨 Python 對(duì)類缺乏真正的封裝(比如,沒有辦法自定義 private 屬性,已經(jīng)給出 public 的 getter 和 setter)。 這可不是真相喲:Python 通過(guò)神奇的方法實(shí)現(xiàn)了大量的封裝,而不是通過(guò)明確的方法或字段修飾符。
請(qǐng)看:
__ getattr__(self, name)
你可以為用戶在試圖訪問(wèn)不存在(不論是存在或尚未建立)的類屬性時(shí)定義其行為。 這對(duì)捕捉和重定向常見的拼寫錯(cuò)誤,給出使用屬性警告是有用的(只要你愿意,你仍舊可選計(jì)算,返回那個(gè)屬性)或拋出一個(gè) AttributeError異常。 這個(gè)方法只適用于訪問(wèn)一個(gè)不存在的屬性,所以,這不算一個(gè)真正封裝的解決之道。
__ setattr__(self, name, value)
不像 __ getattr__,__ setattr__ 是一個(gè)封裝的解決方案。 它允許你為一個(gè)屬性賦值時(shí)候的行為,不論這個(gè)屬性是否存在。 這意味著你可以給屬性值的任意變化自定義規(guī)則。 然而,你需要在意的是你要小心使用 __ setattr__,在稍后的列表中會(huì)作為例子給出。
__ delattr__
這等價(jià)于 __ setattr__, 但是作為刪除類屬性而不是 set 它們。 它需要相同的預(yù)防措施,就像 __ setattr__,防止無(wú)限遞歸(當(dāng)在 __ delattr__ 中調(diào)用 del self.name 會(huì)引起無(wú)限遞歸)。
__ getattribute__(self, name)
__ getattribute__ 良好地適合它的同伴們 __ setattr__ 和 __ delattr__。 可我卻不建議你使用它。__ getattribute__ 只能在新式類中使用(在 Python 的最新版本中,所有的類都是新式類,在稍舊的版本中你可以通過(guò)繼承 object 類來(lái)創(chuàng)建一個(gè)新式類。 它允許你定規(guī)則,在任何時(shí)候不管一個(gè)類屬性的值那時(shí)候是否可訪問(wèn)的。) 它會(huì)因?yàn)樗耐橹械某鲥e(cuò)連坐受到某些無(wú)限遞歸問(wèn)題的困擾(這時(shí)你可以通過(guò)調(diào)用基類的__ getattribute__ 方法來(lái)防止發(fā)生)。 當(dāng) __ getattribute__ 被實(shí)現(xiàn)而又只調(diào)用了該方法如果__ getattribute__ 被顯式調(diào)用或拋出一個(gè) AttributeError 異常,同時(shí)也主要避免了對(duì) __ getattr__ 的依賴。 這個(gè)方法可以使用(畢竟,這是你自己的選擇),不過(guò)我不推薦它是因?yàn)樗幸粋€(gè)小小的用例(雖說(shuō)比較少見,但我們需要特殊行為以獲取一個(gè)值而不是賦值)以及它真的很難做到實(shí)現(xiàn) 0bug。
你可以很容易地在你自定義任何類屬性訪問(wèn)方法時(shí)引發(fā)一個(gè)問(wèn)題。參考這個(gè)例子:
def __setattr__(self, name, value): self.name = value # 當(dāng)每次給一個(gè)類屬性賦值時(shí),會(huì)調(diào)用__setattr__(),這就形成了遞歸 # 因?yàn)樗嬲暮x是 self.__setattr__('name', value) # 所以這方法不停地調(diào)用它自己,變成了一個(gè)無(wú)法退出的遞歸最終引發(fā)crashdef __setattr__(self, name, value): self.__dict__[name] = value # 給字典中的name賦值 # 在此自定義行為再一次,Python 的神奇方法向我們展示了其難以置信的能力,同時(shí)巨大的力量也伴隨著重大的責(zé)任。 重要的是讓你明白正確使用神奇方法,這樣你就不會(huì)破壞其他代碼。
那么,我們?cè)陉P(guān)于定制類屬性訪問(wèn)中學(xué)習(xí)了什么? 不要輕易地使用,事實(shí)上它過(guò)于強(qiáng)大以及反直覺。 這也是它為何存在的理由:Python 尋求干壞事的可能性,但會(huì)把它們弄得很難。 自由是至高無(wú)上的,所以你可以做任何你想做的事情。 以下是一個(gè)關(guān)于特殊屬性訪問(wèn)方法的實(shí)際例子(注意,我們使用 super 因?yàn)椴⒎撬蓄惗加?__ dict__類屬性):
class AccessCounter: '''一個(gè)類包含一個(gè)值和實(shí)現(xiàn)了一個(gè)訪問(wèn)計(jì)數(shù)器。 當(dāng)值每次發(fā)生變化時(shí),計(jì)數(shù)器+1'''def __init__(self, val): super(AccessCounter, self).__setattr__('counter',0) super(AccessCounter, self).__setattr__('value', val)def __setattr__(self, name, value): if name == 'value': super(AccessCounter, self).__setattr__('counter', self.counter + 1) # Make this unconditional. # 如果你想阻止其他屬性被創(chuàng)建,拋出AttributeError(name)異常 super(AccessCounter, self).__setattr__(name, value)def __delattr__(self, name) if name == 'value': super(AccessCounter, self).__setattr__('counter', self.counter + 1) super(AccessCounter, self).__delattr__(name)6.制作自定義序列
很有多種方式可以讓你的類表現(xiàn)得像內(nèi)建序列(字典,元組,列表,字符串等)。 這些是我迄今為止最喜歡的神奇方法了,因?yàn)椴缓侠淼目刂扑鼈冑x予了你一種魔術(shù)般地讓你的類實(shí)例整個(gè)全局函數(shù)數(shù)組漂亮工作的方式。 在我們開始講解這個(gè)內(nèi)容之前,讓我們先快速理清需求。
需求
現(xiàn)在我們正在談?wù)撊绾蝿?chuàng)建你自己的序列。 也是什么談一談 protocol 了。 protocol 在某些地方跟接口很相似。 接口在其他語(yǔ)言中,是一組給定的方法,而你必須定義它們。 然而,在 Python 中 protocol 是完全非正式的,而且不要求顯式聲明去實(shí)現(xiàn)。 更進(jìn)一步說(shuō),它們更像是準(zhǔn)則。
為何我們現(xiàn)在要談?wù)?protocol? 因?yàn)樵?Python 中要實(shí)現(xiàn)自定義容器類型會(huì)涉及使用到這其中某些 protocol。
首先,有一個(gè) protocol 是為定義不可變?nèi)萜鞯?#xff1a;為了制作一個(gè)不可變?nèi)萜?#xff0c;你只需要定義 __ len__ 和__ getitem__(稍后詳述)。 可變?nèi)萜?protocol 要求所有不可變?nèi)萜髟黾?__ setitem__ 和 __ delitem__。 然后,如果你希望你的對(duì)象是可迭代的,那你還得定義一個(gè)會(huì)返回迭代器 iterator 的 __ iter__ 方法。 并且這個(gè)迭代器必須遵守一個(gè)迭代 protocol,也就是要求迭代器有回調(diào)方法 __ iter__ (返回自身)和 next。
隱藏在容器背后的魔法
已經(jīng)迫不及待了?以下便是容器使用的神奇魔法:
__ len__(self)返回容器的長(zhǎng)度。部分 protocol 同時(shí)支持可變和不可變?nèi)萜鱛_ getitem__(self, key)定義當(dāng)某一個(gè) item 被訪問(wèn)時(shí)的行為,使用 self[key] 表示法。 這個(gè)同樣也是部分可變和不可變?nèi)萜?protocol。 這也可拋出適當(dāng)?shù)漠惓?span id="ze8trgl8bvbq" class="token punctuation">: TypeError 當(dāng) key 的類型錯(cuò)誤,或沒有值對(duì)應(yīng) Key 時(shí)。__ setitem__(self, key, value)定義當(dāng)某一個(gè) item 被賦值時(shí)候的行為,使用 self[key]=value 表示法。 這也是部分可變和不可變?nèi)萜?protocol。 再一次重申,你應(yīng)當(dāng)在適當(dāng)之處拋出 KeyError 和 TypeError 異常。__delitem__(self, key)定義當(dāng)某一個(gè) item 被刪除(例如 del self[key])時(shí)的行為。 這僅是部分可變?nèi)萜鞯?protocol。在一個(gè)無(wú)效key 被使用后,你必須拋出一個(gè)合適的異常。__ iter__(self)應(yīng)該給容器返回一個(gè)迭代器。 迭代器會(huì)返回若干內(nèi)容,大多使用內(nèi)建函數(shù) iter() 表示。 當(dāng)一個(gè)容器使用形如 for x in container: 的循環(huán)。 迭代器本身就是其對(duì)象,同時(shí)也要定義好一個(gè) __iter__ 方法來(lái)返回自身。__ reversed__(self)當(dāng)定義調(diào)用內(nèi)建函數(shù) reversed() 時(shí)的行為。應(yīng)該返回一個(gè)反向版本的列表。__ contains__(self, item)__ contains__ 為成員關(guān)系,用 in 和 not in 測(cè)試時(shí)定義行為。 那你會(huì)問(wèn)這個(gè)為何不是一個(gè)序列的 protocol 的一部分? 這是因?yàn)楫?dāng) __contains__ 未定義,Python 就會(huì)遍歷序列,如果遇到正在尋找的 item 就會(huì)返回True。__ concat__(self, other)最后,你可通過(guò) __concat__ 定義你的序列和另外一個(gè)序列的連接。 應(yīng)該從 self 和 other 返回一個(gè)新構(gòu)建的序列。 當(dāng)調(diào)用 2 個(gè)序列時(shí) __concat__ 涉及操作符 +一個(gè)例子
在我們的例子中,讓我們看一下一個(gè) list 實(shí)現(xiàn)的某些基礎(chǔ)功能性的構(gòu)建。 可能會(huì)讓你想起你使用的其他語(yǔ)言(比如 Haskell)。
class FunctionalList: '''類覆蓋了一個(gè)list的某些額外的功能性魔法,像head, tail,init,last,drop,and take''' def __init__(self, values=None): if values is None: self.values = [] else: self.values = valuesdef __len__(self): return len(self.values)def __getitem__(self, key): # 如果key是非法的類型和值,那么list valuse會(huì)拋出異常 return self.values[key]def __setitem__(self, key, value): self.values[key] = valuedef __delitem__(self, key): del self.values[key]def __iter__(self): return iter(self.values)def __reversed__(self): return reversed(self.values)def append(self, value): self.values.append(value) def head(self): # 獲得第一個(gè)元素 return self.values[0] def tail(self): # 獲得在第一個(gè)元素后的其他所有元素 return self.values[1:] def init(self): # 獲得除最后一個(gè)元素的序列 return self.values[:-1] def last(last): # 獲得最后一個(gè)元素 return self.values[-1] def drop(self, n): # 獲得除前n個(gè)元素的序列 return self.values[n:] def take(self, n): # 獲得前n個(gè)元素 return self.values[:n]通過(guò)這個(gè)(輕量的)有用的例子你知道了如何實(shí)現(xiàn)你自己的序列。 當(dāng)然,還有很多更有用的應(yīng)用,但是它們其中的很多已經(jīng)被標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)了,像 Counter, OrderedDict, NamedTuple
7.反射
你也可以通過(guò)定義神奇方法來(lái)控制如何反射使用內(nèi)建函數(shù) isinstance() 和 issubclass() 的行為。 這些神奇方法是:
__instancecheck__(self, instance)檢查一個(gè)實(shí)例是否是你定義類中的一個(gè)實(shí)例(比如,isinstance(instance, class))__subclasscheck__(self, subclass)檢查一個(gè)類是否是你定義類的子類(比如,issubclass(subclass, class))這對(duì)于神奇方法的用例情況來(lái)說(shuō)可能較小,可它的確是真的。 我并不想花費(fèi)太多的時(shí)間在反射方法上面,因?yàn)樗麄儾皇悄敲吹刂匾?不過(guò)它們反映了在 Python 中關(guān)于面對(duì)對(duì)象編程一些重要的東西,而且在 Python 中的普遍:總是在找一種簡(jiǎn)單的方式來(lái)做某些事情,即使它能被用到的不多。 這些神奇方法似乎看上去不那么有用,但當(dāng)你需要使用它們的時(shí)候你會(huì)感激它們的存在(和你閱讀的這本指南!)。
8.可調(diào)用對(duì)象
正如你可能已經(jīng)知道,在 Python 中函數(shù)是第一類對(duì)象。 這就意味著它們可以被傳遞到函數(shù)和方法,就像是任何類型的對(duì)象。 這真是一種難以置信強(qiáng)大的特性。
這是 Python 中一個(gè)特別的神奇方法,它允許你的類實(shí)例像函數(shù)。 所以你可以“調(diào)用”它們,把他們當(dāng)做參數(shù)傳遞給函數(shù)等等。 這是另一個(gè)強(qiáng)大又便利的特性讓 Python 的編程變得更可愛了。
__ call__(self, [args…])
允許類實(shí)例像函數(shù)一樣被調(diào)用。 本質(zhì)上,這意味著 x() 等價(jià)于 x.__ call__()。 注意,__ call__ 需要的參數(shù)數(shù)目是可變的,也就是說(shuō)可以對(duì)任何函數(shù)按你的喜好定義參數(shù)的數(shù)目定義 __ call__
__ call__ 可能對(duì)于那些經(jīng)常改變狀態(tài)的實(shí)例來(lái)說(shuō)是極其有用的。 “調(diào)用”實(shí)例是一種順應(yīng)直覺且優(yōu)雅的方式來(lái)改變對(duì)象的狀態(tài)。 下面一個(gè)例子是一個(gè)類表示一個(gè)實(shí)體在一個(gè)平面上的位置:
''' 遇到問(wèn)題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:857662006 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' class Entity: '''描述實(shí)體的類,被調(diào)用的時(shí)候更新實(shí)體的位置'''def __init__(self, size, x, y): self.x, self.y = x, y self.size = sizedef __call__(self, x, y): '''改變實(shí)體的位置''' self.x, self.y = x, y#省略...9.上下文管理
在 Python2.5 里引入了一個(gè)新關(guān)鍵字(with)使得一個(gè)新方法得到了代碼復(fù)用。 上下文管理這個(gè)概念在 Python 中早已不是新鮮事了(之前它作為庫(kù)的一部分被實(shí)現(xiàn)過(guò)),但直到 PEP343 才作為第一個(gè)類語(yǔ)言結(jié)構(gòu)取得了重要地位而被接受。 你有可能早就已經(jīng)見識(shí)過(guò) with 聲明:
with open('foo.txt') as bar: # 對(duì)bar執(zhí)行某些動(dòng)作上下文管理允許對(duì)對(duì)象進(jìn)行設(shè)置和清理動(dòng)作,用 with 聲明進(jìn)行已經(jīng)封裝的操作。 上下文操作的行為取決于 2 個(gè)神奇方法:
__ enter__(self)
定義塊用 with 聲明創(chuàng)建出來(lái)時(shí)上下文管理應(yīng)該在塊開始做什么。 注意,enter 的返回值必須綁定 with 聲明的目標(biāo),或是 as 后面的名稱。
__ exit__(self, exception_type, exception_value, traceback)
定義在塊執(zhí)行(或終止)之后上下文管理應(yīng)該做什么。 它可以用來(lái)處理異常,進(jìn)行清理,或行動(dòng)處于塊之后某些總是被立即處理的事。 如果塊執(zhí)行成功的話,excepteion_type,exception_value,和 traceback 將會(huì)置None。 否則,你可以選擇去處理異常,或者讓用戶自己去處理。 如果你想處理,確保在全部都完成之后__ exit__ 會(huì)返回 True。 如果你不想讓上下文管理處理異常,那就讓它發(fā)生好了。
__ enter__ 和 __ exit__ 對(duì)那些已有良好定義和對(duì)設(shè)置,清理行為有共同行為的特殊類是有用。 你也可以使用這些方法去創(chuàng)建封裝其他對(duì)象通用的上下文管理。 看下面的例子:
class Closer: '''用with聲明一個(gè)上下文管理用一個(gè)close方法自動(dòng)關(guān)閉一個(gè)對(duì)象'''def __init__(self, obj): self.obj = objdef __enter__(self): return self.obj # 綁定目標(biāo)def __exit__(self, exception_type, exception_val, trace): try: self.obj.close() except AttributeError: #obj不具備close print 'Not closable.' return True # 成功處理異常以下是一個(gè)對(duì)于 Closer 實(shí)際應(yīng)用的一個(gè)例子,使用一個(gè) FTP 連接進(jìn)行的演示(一個(gè)可關(guān)閉的套接字):
>>> from magicmethods import Closer >>> from ftplib import :;; >>> with Closer(FTP('ftp.somsite.com')) as conn: ... conn.dir() ... # 省略的輸出 >>> conn.dir() # 一個(gè)很長(zhǎng)的AttributeError消息, 不能關(guān)閉使用的一個(gè)連接 >>> with Closer(int(5)) as i: ... i += 1 ... Not closeable. >>> i 6瞧見我們?nèi)绾纹恋胤庋b處理正確或不正確的用例了嗎?那就是上下文管理和神奇方法的威力。
10.構(gòu)建描述符對(duì)象
描述符可以改變其他對(duì)象,也可以是訪問(wèn)類中任一的 getting,setting,deleting。 描述符不意味著孤立;相反,它們意味著會(huì)被它們的所有者類控制。 當(dāng)建立面向?qū)ο髷?shù)據(jù)庫(kù)或那些擁有相互依賴的屬性的類時(shí),描述符是有用的。 當(dāng)描述符在幾個(gè)不同單元或描述計(jì)算屬性時(shí)顯得更為有用。
作為一個(gè)描述符,一個(gè)類必須至少實(shí)現(xiàn) __ get__,__ set__,和 __delete__中的一個(gè)。 讓我們快點(diǎn)看一下這些神奇方法吧:
__ get__(self, instance, owner)
當(dāng)描述符的值被取回時(shí)定義其行為。instance 是 owner 對(duì)象的一個(gè)實(shí)例,owner 是所有類。
__ set__(self, instance, value)
當(dāng)描述符的值被改變時(shí)定義其行為。instance 是 owner 對(duì)象的一個(gè)實(shí)例,value 是設(shè)置的描述符的值
__ delete__(self, instance)
當(dāng)描述符的值被刪除時(shí)定義其行為。instance 是 owner 對(duì)象的一個(gè)實(shí)例。
現(xiàn)在,有一個(gè)有用的描述符應(yīng)用例子:單位轉(zhuǎn)換策略
''' 遇到問(wèn)題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:857662006 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' class Meter(object): '''米描述符'''def __init__(self, value=0.0): self.value = float(value) def __get__(self, instance, owner): return self.value def __set__(self, instance, value): self.value = float(value)class Foot(object): '''英尺描述符'''def __get__(self, instance, owner): return instance.meter * 3.2808 def __set__(self, instance, value): instance.meter = float(value) / 3.2808class Distance(object): '''表示距離的類,控制2個(gè)描述符:feet和meters''' meter = Meter() foot = Foot()11.Pickling 你的對(duì)象
假如你花時(shí)間和其他 Pythonistas 打交道,那么你至少有可能聽到過(guò) Pickling 這個(gè)詞。 Pickling 是一種對(duì) Python 數(shù)據(jù)結(jié)構(gòu)的序列化過(guò)程。 如果你需要存儲(chǔ)一個(gè)對(duì)象,之后再取回它(通常是為了緩存)那么它就顯得格外地有用了。 同時(shí),它也是產(chǎn)生憂慮和困惑的主要來(lái)源。
Pickling 是那么地重要以至于它不僅有自己專屬的模塊(pickle),還有自己的 protocol 和神奇方法與其相伴。 但首先用簡(jiǎn)要的文字來(lái)解釋下如何 pickle 已經(jīng)存在的類型(如果你已經(jīng)懂了可以隨意跳過(guò)這部分內(nèi)容)
Pickling:鹽水中的快速浸泡
讓我們跳入 pickling。 話說(shuō)你有一個(gè)詞典你想要保存它并在稍后取回。 你可以把它的內(nèi)容寫到一個(gè)文件中去,需要非常小心地確保你寫了正確的語(yǔ)法,然后用 exec() 或處理文件的輸入取回寫入的內(nèi)容。 但這是不穩(wěn)定的:如果你你在純文本中保存重要的數(shù)據(jù),它有可能被幾種方法改變,導(dǎo)致你的程序 crash 或在你的計(jì)算機(jī)上運(yùn)行了惡意代碼而出錯(cuò)。 于是,我們準(zhǔn)備 pickle 它:
import pickledata = {'foo': [1,2,3], 'bar': ('Hello','world!'), 'baz': True} jar = open('data.pk1', 'wb') pickle.dump(data, jar) # 把pickled數(shù)據(jù)寫入jar文件 jar.close()好了現(xiàn)在,已經(jīng)過(guò)去了幾個(gè)小時(shí)。 我們希望拿回?cái)?shù)據(jù),而我們需要做的事僅僅是 unpickle 它:
import picklepk1_file = open('data.pk1','rb') #連接pickled數(shù)據(jù) data = pickle.load(pk1_file) #把數(shù)據(jù)load到一個(gè)變量中去 print data pk1_file.close()發(fā)生了什么事?正如你的預(yù)期,我們獲得了 data。
現(xiàn)在,我要給你一些忠告:pickling 并非完美。 Pickle 文件很容易因意外或出于故意行為而被損毀。 Pickling 可能比起使用純文本文件安全些,但它仍舊有可能會(huì)被用來(lái)跑惡意代碼。 還有因?yàn)?Python 版本的不兼容問(wèn)題,所以不要期望發(fā)布 Pickled 對(duì)象,也不要期望人們能夠打開它們。 但是,它依然是一個(gè)強(qiáng)大的緩存工具和其他常見序列化任務(wù)。
Pickling你自定義的對(duì)象
Pickling 不僅可用在內(nèi)建類型上,還可以用于遵守 pickle 協(xié)議的任何類。 pickle 協(xié)議有 4 個(gè)可選方法用于定制 Python 對(duì)象如何運(yùn)行(這跟 C 擴(kuò)展有點(diǎn)不同,但那不在我們討論的范圍內(nèi)):
__ getinitargs__(self)如果你想當(dāng)你的類 unpickled 時(shí)調(diào)用 __ init__,那你可以定義__ getinitargs__,該方法應(yīng)該返回一個(gè)元組的參數(shù),然后你可以把他傳遞給 __ init__。注意,該方法僅適用于舊式類。__ getnewargs__(self)對(duì)于新式類,你可以影響有哪些參數(shù)會(huì)被傳遞到 __new__ 進(jìn)行 unpickling。 該方法同樣應(yīng)該返回一個(gè)元組參數(shù),然后能傳遞給 __new____getstate__(self)代替對(duì)象的 __dict__ 屬性被保存。 當(dāng)對(duì)象 pickled,你可返回一個(gè)自定義的狀態(tài)被保存。 當(dāng)對(duì)象 unpickled 時(shí),這個(gè)狀態(tài)將會(huì)被 __setstate__ 使用。__setstate__(self, state)對(duì)象 unpickled 時(shí),如果 __setstate__ 定義對(duì)象狀態(tài)會(huì)傳遞來(lái)代替直接用對(duì)象的 __dict__ 屬性。 這正好跟__getstate__ 手牽手:當(dāng)二者都被定義了,你可以描述對(duì)象的 pickled 狀態(tài),任何你想要的。一個(gè)例子:
我們的例子是 Slate 類,它會(huì)記憶它曾經(jīng)的值和已經(jīng)寫入的值。 然而,當(dāng)這特殊的 slate 每一次 pickle 都會(huì)被清空:當(dāng)前值不會(huì)被保存。
''' 遇到問(wèn)題沒人解答?小編創(chuàng)建了一個(gè)Python學(xué)習(xí)交流QQ群:857662006 尋找有志同道合的小伙伴,互幫互助,群里還有不錯(cuò)的視頻學(xué)習(xí)教程和PDF電子書! ''' import timeclass Slate: '''存儲(chǔ)一個(gè)字符串和一個(gè)變更log,當(dāng)Pickle時(shí)會(huì)忘記它的值'''def __init__(self, value): self.value = value self.last_change = time.asctime() self.history = {}def change(self, new_value): # 改變值,提交最后的值到歷史記錄 self.history[self.last_change] = self.value self.value = new_value self.last_change = time.asctime()def print_changes(self): print 'Changelog for Slate object:' for k, v in self.history.items(): print '%st %s' % (k, v)def __getstate__(self): # 故意不返回self.value 或 self.last_change. # 當(dāng)unpickle,我們希望有一塊空白的"slate" return self.historydef __setstate__(self, state): # 讓 self.history = state 和 last_change 和 value被定義 self.history = state self.value, self.last_change = None, None12.總結(jié)
這份指南的目標(biāo)就是任何人讀一讀它,不管讀者們是否具備 Python 或面對(duì)對(duì)象的編程經(jīng)驗(yàn)。 如果你正準(zhǔn)備學(xué)習(xí) Python,那你已經(jīng)獲得了編寫功能豐富,優(yōu)雅,易用的類的寶貴知識(shí)。 如果你是一名中級(jí) Python 程序員,你有可能已經(jīng)拾起了一些新概念和策略和一些好的方法來(lái)減少你和你的用戶編寫的代碼量。 如果你是一名 Pythonista 專家,你可能已經(jīng)回顧了某些你可能已經(jīng)被你遺忘的知識(shí)點(diǎn),或著你又學(xué)習(xí)到了一些新技巧。 不管你的的經(jīng)驗(yàn)等級(jí),我希望這次 Python 神奇方法的旅程達(dá)到了真正神奇的效果。(我無(wú)法控制自己在最后不用個(gè)雙關(guān)語(yǔ))
附錄:如果調(diào)用神奇方法
Python 中的一些神奇方法直接映射到內(nèi)建函數(shù);在這種情況下,調(diào)用它們的方法是相當(dāng)明顯的。 然而,在其他情況下,那些調(diào)用方法就不這么明顯了。 本附錄致力于揭開能夠引導(dǎo)神奇方法被調(diào)用的非明顯語(yǔ)法。
神奇方法 調(diào)用方法 說(shuō)明
希望這張表格可以幫你掃清你有關(guān)語(yǔ)法涉及到神奇方法的問(wèn)題。
總結(jié)
以上是生活随笔為你收集整理的Python 内编写类的各种技巧和方法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Python super超类方法
- 下一篇: python的68个内置函数