__slots__
如果你看過github當中一些大牛的代碼,你會發現很多大牛經常在類的頂部加上__slots__關鍵字。如果你足夠好奇,你可能會試着把這個關鍵字去掉再運作試試,你會發現去掉了之後什麼也沒有發生,一切依然運作得很好。
那麼這個__slots__關鍵字究竟是做什麼的呢?
它主要有兩個功能,我們先來說第一個功能,就是限制使用者的使用。
我們都知道Python是一門非常靈活的動态語言,很多在其他語言看起來完全不能容忍的事情在Python當中是可行的,這也是Python的設計理念,為了靈活和代碼友善犧牲了效率。比如我們來看一個很簡單的例子,由于Python是動态語言,是以類的成員甚至可以在類建立好了之後動态建立。這在靜态語言當中是絕對不行的,我們隻能調用類當中已有的屬性,是不能或者很難添加新屬性的。
比如這段代碼:
class Exp:
def __init__(self):
self.a = None
self.b = None
if __name__ == "__main__":
exp = Exp()
exp.c = 3
print(exp.c)
複制
我們定義了一個類叫做Exp,我們為它建立了a和b兩個成員。但是我們在使用的時候,對c成員進行了指派。要知道Exp類當中是沒有成員c的,但是程式并不會報錯,我們這麼運作了之後它會将c添加進這個執行個體當中。
從一方面來看,這當然非常靈活,但是另一方面,這也留下了隐患。如果使用者随意添加屬性,可能會導緻未知的問題,尤其在複雜的系統當中。是以有些時候為了嚴謹,我們會不希望使用者做這種動态的修改。__slots__正是用來做這個的。
我們把這個關鍵字加上,再來運作結果就不一樣了:
class Exp:
__slots__ = ['a', 'b']
def __init__(self):
self.a = None
self.b = None
if __name__ == "__main__":
exp = Exp()
exp.c = 3
print(exp.c)
複制
如果你運作這段代碼的話,你會得到一個報錯,提示你Exp這個對象當中并沒有c這個成員,也就是說我們隻能運用__slots__這個關鍵字當中定義的成員,對于沒有定義的成員不能随意建立,這樣就限制了使用者的使用。
雖然現在大部分人使用這個關鍵字都是報着這個目的,但是很遺憾的是,Python建立者的初衷其實并不是這個。這就談到了__slots__關鍵字的第二個作用,就是節省記憶體。
如果了解過Python底層的實作原理,你會發現在Python當中為每一個執行個體都建立了一個字典,就是大名鼎鼎的__dict__字典。正是因為背後有一個字典,是以我們才可以創造出原本不存在的成員,也才支援這樣動态的效果。我們可以人工地調用這個字典輸出其中的内容,我們在加上__slots__關鍵字之前,輸出的結果是這樣的:
{'a': None, 'b': None}
複制
但是加上了這個關鍵字之後,會得到一個報錯,會告訴你Exp這個對象當中沒有__dict__這個成員。原因很簡單,因為使用dict來維護執行個體,會消耗大量的記憶體,額外存儲了許多資料,而使用__slots__之後,Python内部将不再為執行個體建立一個字典來維護,而是會使用一個固定大小的數組,這樣就節省了大量的空間。這個節省可不是一點半點,一般可以節省一半以上。也就是說犧牲了一定的靈活性,保證了性能。這一點也是__slots__這個關鍵字設計的初衷,但是現在很多人都用錯了地方。
property
這個關鍵字在的文章當中曾經提到過,不過很不好意思的是,由于之前寫文章的時候對它的了解還很有限,導緻一些闡述存在一些謬誤,是以這裡再提一下這個關鍵字的運用作為彌補。
property可以幫我們綁定類當中一些屬性的指派和擷取,也就是get和set。我們來看個例子:
class Exp:
def __init__(self, param):
self.param = param
@property
def param(self):
return self._param
@param.setter
def param(self, value):
self._param = value
複制
這裡的property注解會在我們調用.param的時候被執行,而param.setter會在我們為param這個屬性指派的時候被執行。是以你可能會奇怪,為什麼我們在__init__方法當中初始化的時候用的是self.param = param而不是self._param = param,這是因為我們在執行前者的時候,Python一樣會調用@param.setter這個注解,是以我們沒有必要寫成後者的形式。當然你也可以這麼寫,不過兩者是完全等價的。
作為一個前Java程式員為類當中所有變量加上get和set方法幾乎成了政治正确,是以我特别喜歡為類當中所有的屬性加上property。但是這是不對的,加上property是非常耗時的,是以如非必要不要這麼做,我們直接調用來進行指派就好了,如果有必要,我們可以手動寫上get和set方法。那麼問題來了,既然不是為了規範,那麼我們又為什麼要用到property呢?
答案很簡單,為了校驗變量類型。
由于Python是動态語言,并且是隐式類型的,是以我們拿到變量的時候并不知道它究竟是什麼類型,也不知道使用者為給它指派成什麼類型。是以在一些情況下我們可能會希望做好限制,告訴使用者隻能将這個變量指派成這個類型,否則就會報錯。通過使用property,我們可以很友善地做到這點。
class Exp:
def __init__(self, param):
self.param = param
@property
def param(self):
return self._param
@param.setter
def param(self, value):
if not isinstance(value, str):
raise TypeError('Want a string')
self._param = value
複制
除此之外,property還有一個用法是代替函數。舉個例子:
class Exp:
def __init__(self, param):
self.param = param
@property
def param(self):
return self._param
@param.setter
def param(self, value):
if not isinstance(value, str):
raise TypeError('Want a string')
self._param = value
@property
def hello(self):
return 'hello ' + self.param
複制
這樣我們就可以通過.hello來代替調用一個函數,這樣做其實是一種動态計算。hello的結果并沒有被存儲起來,之後當我們調用的時候才會執行,在一些場景下這樣做會非常友善。
命名規範
最後我們來看下Python對象當中的命名規範,在之前的文章當中我們曾經說過,在Python當中沒有對public和private的字段做區分,所有的字段都是public的,也就是說使用者可以拿到類當中所有的字段和方法。為了規範,程式員們約定俗成,決定所有加了下劃線的方法和變量都看成是private的,即使我們能調用,但是一般情況下我們也不這麼幹。
是以我們通常會寫兩個方法,一個是公開的接口,一個是内部的實作。我們調用的時候隻調用公開的接口,公開的接口再去調用内部的實作。這在Python當中已經成了慣例,因為我們在調用内部方法的時候,往往還會傳入一些内部的參數。
我們來看個簡單的例子:
class ExpA:
def __init__(self):
pass
def public_func(self):
self._private_func()
def _private_func(self):
print('private ExpA')
if __name__ == "__main__":
exp = ExpA()
exp.public_func()
複制
除了_之外我們經常還會看到一些兩個下劃線的變量和方法,那麼它們之間又有什麼差別呢?
為了回答這個問題,我們來看下面這個例子:
class ExpA:
def __init__(self):
pass
def public_func(self):
self.__private_func()
def __private_func(self):
print('private ExpA')
class ExpB(ExpA):
def __init__(self):
pass
def public_func(self):
self.__private_func()
def __private_func(self):
print('private ExpB')
if __name__ == "__main__":
exp = ExpB()
exp.public_func()
exp._ExpB__private_func()
exp._ExpA__private_func()
複制
請問最後會輸出什麼?
我們試一下就知道,第一行輸出的是private ExpB,這個沒有問題。但是後面兩個是什麼?
後面兩個就是__private_func,隻不過系統自動将它重新命名了。重新命名的原因也很簡單,因為Python禁止加了兩個下劃線的方法被子類覆寫。是以這兩者的差別就在這裡,它們都被認為是private的方法和屬性,但是一個下劃線允許子類覆寫,而兩個下劃線不行。是以如果我們在開發的時候希望我們某一個方法不會被子類覆寫,那麼我們就需要加上兩個下劃線。
最後,我們來看一個小問題。在C++當中當我們的變量名和系統的關鍵字沖突的時候,我們往往會在變量前面加上一個_來作為區分。但是由于Python當中下劃線被賦予了含義,是以我們不能這麼幹,那麼當變量沖突的時候應該怎麼辦呢?答案也很簡單,我們可以把下劃線加在後面,比如lambda_。
總結
回顧一下今天的内容,主要是__slots__, property和下劃線在類當中的使用。這三者都是Python面向對象當中經常用到的知識,了解它們不但可以讓我們寫出更規範的代碼,也有助于幫助我們了解其他大牛的源碼,是以是非常必要的。