Python 數(shù)據(jù)模型
一、如何理解數(shù)據(jù)模型?
最近我在閱讀一本專門(mén)講述 Python 語(yǔ)言特性的書(shū)(本文部分內(nèi)容來(lái)自?Fluent Python?這本書(shū)),書(shū)中提到了數(shù)據(jù)模型這個(gè)詞,數(shù)據(jù)模型是不是我們經(jīng)常說(shuō)的數(shù)據(jù)類型?其實(shí)不是,數(shù)據(jù)模型是對(duì) Python 框架的描述,他規(guī)范了自身構(gòu)建模塊的接口,這些接口我們可以理解為是 Python 中的特殊方法,例如?__iter__、__len__、__del__?等。這些模塊包括但不限于序列、迭代器、函數(shù)、類和上下文管理器。假如我們?cè)谟懻?,擁有哪些方法和屬性的?duì)象可以稱為序列,實(shí)際上我們就是在討論序列的數(shù)據(jù)模型。
Python 最好的品質(zhì)之一是一致性,當(dāng)你使用 Python 一段時(shí)間以后,你就會(huì)開(kāi)始理解 Python 語(yǔ)言,并且能正確的猜出對(duì)你來(lái)說(shuō)是全新的語(yǔ)言特性。
如果你帶著其他編程語(yǔ)言的經(jīng)歷來(lái)使用 Python ,你可能為?len(object)?而不是object.len()?而感到好奇,在?Java?語(yǔ)言中常常使用?object.len()?這種方法取得對(duì)象的長(zhǎng)度。當(dāng)你進(jìn)一步的理解這種不適感背后的強(qiáng)大之處的時(shí)候,你會(huì)被 Python 的設(shè)計(jì)哲學(xué)所折服,這正是建立在 Python 數(shù)據(jù)模型之上的結(jié)果,Python 數(shù)據(jù)模型的 API ,為我們使用地道的 Python 特性構(gòu)建對(duì)象提供了工具,這正是我們常常說(shuō)的?Pythonic。
不管在哪種框架下寫(xiě)程序,都會(huì)花費(fèi)大量的時(shí)間區(qū)實(shí)現(xiàn)那些會(huì)被框架本身調(diào)用的方法,Python 框架本身也不例外。當(dāng)你在使用?object[item]?的時(shí)候,背后實(shí)際上是調(diào)用了object.__getitem__?這個(gè)方法。這樣的好處的是什么,這樣我們就可以對(duì)自建的對(duì)象使用?[]運(yùn)算符,我們只需要在類當(dāng)中定義?__getitem__?方法即可。
__getitem__?是 Python 的特殊方法之一,常見(jiàn)的特殊方法還有__len__、__iter__、__enter__、__call__?等
這些特殊方法,能讓你自己的對(duì)象實(shí)現(xiàn)和支持以下的語(yǔ)言架構(gòu),并與之交互。
迭代
集合類
屬性訪問(wèn)
運(yùn)算符重載
函數(shù)和方法的調(diào)用
對(duì)象的創(chuàng)建和銷毀
字符串表示形式和格式化
上下文管理器
二、實(shí)現(xiàn)自己的序列類
數(shù)據(jù)模型提供了使用 Python 語(yǔ)言特性的來(lái)構(gòu)建對(duì)象的 API ,那么我們嘗試著實(shí)現(xiàn)自己的序列類。
class?MyList(object):
????
????def?__init__(self,?):
????????self.items?=?[str(x)?for?x?in?range(10)]
????????self.len?=?10
????def?__getitem__(self,?item):
????????return?self.items[item]
????def?__len__(self):
????????return?self.len
a?=?MyList()
for?x?in?a:
????print(x)
print('第一個(gè)元素',?a[0])
print("長(zhǎng)度:",?len(a))
我們自己定義的類支持迭代,支持切片,也支持len?方法,這其實(shí)就是實(shí)現(xiàn)了 Python 序列的協(xié)議(非正式接口),我們通過(guò)數(shù)據(jù)模型實(shí)現(xiàn)了自定義的序列,而非子類化內(nèi)置序列,這其實(shí)也是鴨子模型的一種語(yǔ)言,當(dāng)某個(gè)類的行為看起來(lái)像鴨子(這里指代序列),那么這個(gè)類就可以說(shuō)是序列。不在乎是通過(guò)子類化,還是序列協(xié)議實(shí)現(xiàn)的。
我們已經(jīng)可以體會(huì)到通過(guò)使用特殊方法來(lái)利用 Python 數(shù)據(jù)模型的好處,作為你的類的用戶,不必去記住標(biāo)準(zhǔn)操作的各式名稱(“怎么得到長(zhǎng)度?” 是 .size() 還是 .length() 還是別的什么 ?)
上面的實(shí)例中,MyList?類可以進(jìn)行迭代和切片,切片的功能是由?__getitem__?提供的,迭代的功能實(shí)際上是由?__iter__?提供的,它返回一個(gè)可迭代對(duì)象。但是?MyList?類中沒(méi)有__iter__?方法,這是怎么回事呢?
因?yàn)槿绻麤](méi)有實(shí)現(xiàn)?__iter__?方法, 但是實(shí)現(xiàn)了__getitem__?方法, Python 會(huì)創(chuàng)建一個(gè)迭代器,嘗試按順序( 從索引 0 開(kāi)始)獲取元素。
現(xiàn)在我們嘗試打印?a?對(duì)象
class?MyList(object):
????
????def?__init__(self,?):
????????self.items?=?[str(x)?for?x?in?range(10)]
????????self.len?=?10
????def?__getitem__(self,?item):
????????return?self.items[item]
????def?__len__(self):
????????return?self.len
a?=?MyList()
print(a)
>>?<__main__.MyList?object?at?0x0000015AEAFC95F8>
結(jié)果是一串我們不在意的內(nèi)存地址,但是我們經(jīng)常發(fā)現(xiàn),一些 Python 內(nèi)置模塊,或者第三方的庫(kù),當(dāng)我們直接打印某個(gè)對(duì)象的時(shí)候,輸出的是我們可以看懂的信息,而不是這樣的內(nèi)存地址。
其實(shí)這個(gè)是可以通過(guò)特殊方法?__str__?來(lái)實(shí)現(xiàn)的,它返回一個(gè)字符串,我們可以通過(guò)為MyList?類添加一個(gè)?__str__?特殊方法實(shí)現(xiàn)。
class?MyList(object):
????def?__init__(self,?):
????????self.items?=?[str(x)?for?x?in?range(10)]
????????self.len?=?10
????def?__getitem__(self,?item):
????????return?self.items[item]
????def?__len__(self):
????????return?self.len
????def?__str__(self):
????????return?','.join(self.items)
a?=?MyList()
print(a)
>>?0,1,2,3,4,5,6,7,8,9三、為什么 len 不是普通方法
如果 x 是一個(gè)內(nèi)置類型的實(shí)例,那么 len(x)的速度會(huì)非常快。背后的原因是 CPython 會(huì)直接從一個(gè) C 結(jié)構(gòu)體里讀取對(duì)象的長(zhǎng)度,完全不會(huì)調(diào)用任何方法。獲取一個(gè)集合中元素的數(shù)量是一個(gè)很常見(jiàn)的操作,在 str、list、memoryview 等類型上,這個(gè)操作必須高效。換句話說(shuō),len 之所以不是一個(gè)普通方法,是為了讓 Python 自帶的數(shù)據(jù)結(jié)構(gòu)可以走后門(mén),abs 也是同理。但是多虧了它是特殊方法,我們也可以把 len 用于自定義數(shù)據(jù)類型。這種處理方式在保持內(nèi)置類型的效率和保證語(yǔ)言的一致性之間找到了一個(gè)平衡點(diǎn),也印證了“ Python 之 ”中的另外一句話:“不能讓特例特殊到開(kāi)始破壞既定規(guī)則?!?/p>
四、數(shù)據(jù)模型與特殊方法
數(shù)據(jù)模型描述的是對(duì)象協(xié)議,而特殊方法正是內(nèi)置對(duì)象的所實(shí)現(xiàn)的協(xié)議,為了讓我們的代碼風(fēng)格表現(xiàn)的和內(nèi)置類型一樣,或者說(shuō)更 Python 風(fēng)格的代碼,我們可以使用特殊方法,而不是子類化。
對(duì)象的一個(gè)基本要求就是它得要有合理的字符串表示形式,我們可以通過(guò)?__str__?和__repr__?來(lái)滿足這個(gè)要求。__repr__?方便我們調(diào)試,__str__?適合給終端用戶看。這就是數(shù)據(jù)模型中存在特殊方法?__repr__?和?__str__?的原因。
Python 中的特殊方法還有很多,這里主要講述的還是數(shù)據(jù)模型,希望大家可以理解 Python 語(yǔ)言的設(shè)計(jì)哲學(xué),以及思考如何寫(xiě)出更?Pythonic?的代碼。
公眾號(hào)推薦:數(shù)據(jù)思踐
數(shù)據(jù)思踐公眾號(hào)記錄和分享數(shù)據(jù)人思考和踐行的內(nèi)容與故事。
《數(shù)據(jù)科學(xué)與人工智能》公眾號(hào)推薦朋友們學(xué)習(xí)和使用Python語(yǔ)言,需要加入Python語(yǔ)言群的,請(qǐng)掃碼加我個(gè)人微信,備注【姓名-Python群】,我誠(chéng)邀你入群,大家學(xué)習(xí)和分享。
關(guān)于Python語(yǔ)言,有任何問(wèn)題或者想法,請(qǐng)留言或者加群討論。
