屬性
類屬性和實(shí)例屬性
屬性是面向?qū)ο蟮慕蟹ǎc變量一樣是用來存放程序運(yùn)行時(shí)需要用到的數(shù)據(jù)。區(qū)別在于,屬性一定有一個(gè)宿主堪嫂,根據(jù)數(shù)組的不同,分為類屬性和實(shí)例屬性:
- 類屬性:屬性的宿主是類對象木柬,類的實(shí)例共享這個(gè)屬性皆串。任何一個(gè)類實(shí)例對類屬性進(jìn)行修改,其他類實(shí)例訪問這個(gè)類屬性的時(shí)候眉枕,值也相應(yīng)的發(fā)生變化恶复。
- 實(shí)例屬性:屬性的宿主是實(shí)例對象,類的實(shí)例和實(shí)例之間各自保存實(shí)例屬性速挑,實(shí)例屬性的修改僅對修改該屬性的實(shí)例生效谤牡。
申明:為了描述上的方便,下文中遵循下面兩個(gè)規(guī)則:
- class.xxxxx - 表示類屬性
- obj.xxxxx - 表示實(shí)例屬性
類屬性和實(shí)例屬性的定義方式分別有兩種姥宝,一種在類內(nèi)部添加拓哟,另一種是在類外部添加,如下面代碼所示:
class ChinesePeople:
# Class Attribute
country = 'China'
def __init__(self, name):
# Instance Attribute
self.name = name
p1 = ChinesePeople('Bill')
print(p1.name, p1.country, ChinesePeople.country)
p1.age = 18
ChinesePeople.color = 'yellow'
print(p1.age, p1.color, ChinesePeople.color)
#### Outputs ###
Bill China China
18 yellow yellow
上面的示例代碼涉及到屬性的創(chuàng)建和訪問伶授。對于屬性的刪除断序,用關(guān)鍵字del即可,即del class.attr/del obj.attr糜烹。實(shí)例屬性只能通過實(shí)例對象來訪問违诗,但是類屬性即可以通過類對象也可以通過實(shí)例對象來訪問。之所以這樣疮蹦,這和屬性的存儲和查找是關(guān)聯(lián)的诸迟。
在Python中,屬性和方法都是保存在dict這個(gè)內(nèi)置的字典中愕乎。相應(yīng)的阵苇,類屬性則保存在class.dict中,實(shí)例屬性保存在obj.dict中感论。因此绅项,對于類屬性和實(shí)例屬性的訪問,遵循以下規(guī)則:
- class.attr:通過類對象訪問類屬性比肄,那么直接去class.dict中查找快耿,找不到則拋出異常。
- obj.attr:通過實(shí)例對象訪問實(shí)例屬性或者類屬性芳绩,遵循一樣的順序掀亥。也就是,先從obj.dict中查找妥色,如果找不過則從class.__dict__(實(shí)際是obj.__class__.__dict__)中查找搪花,如果還是找不到,則拋出異常。
- 相應(yīng)的撮竿,當(dāng)適用del關(guān)鍵字刪除實(shí)例屬性或者類屬性的時(shí)候丁稀,對應(yīng)的從相應(yīng)的dict中刪除對應(yīng)項(xiàng)。
我們可以通過打印出dict的值來了解上面的規(guī)則:
print(p1.__dict__)
print(ChinesePeople.__dict__)
del p1.name
del ChinesePeople.country
print(p1.__dict__)
print(ChinesePeople.__dict__)
#### Outputs ###
# p1.__dict__
{'name': 'Bill', 'age': 18}
#ChinesePeople.__dict__
{'__module__': '__main__', 'country': 'China', '__init__': <function ChinesePeople.__init__ at 0x10df72a60>, '__dict__': <attribute '__dict__' of 'ChinesePeople' objects>, '__weakref__': <attribute '__weakref__' of 'ChinesePeople' objects>, '__doc__': None, 'color': 'yellow'}
# p1.__dict__
{'age': 18}
#ChinesePeople.__dict__
{'__module__': '__main__', '__init__': <function ChinesePeople.__init__ at 0x10d6c5a60>, '__dict__': <attribute '__dict__' of 'ChinesePeople' objects>, '__weakref__': <attribute '__weakref__' of 'ChinesePeople' objects>, '__doc__': None, 'color': 'yellow'}
上面提到倚聚,通過實(shí)例對象訪問類屬性是完全沒有問題的线衫,而且實(shí)際中也經(jīng)常這么做。那可不可以通過實(shí)例對象來修改類屬性呢惑折?答案是不可以的授账,這樣做相當(dāng)于添加了一個(gè)實(shí)例屬性,這一點(diǎn)也可以通過查看obj.dict得以驗(yàn)證惨驶。
print(p1.__dict__)
p1.country = 'Great China'
print(p1.__dict__)
#### Outputs ###
{'name': 'Bill'}
{'name': 'Bill', 'country': 'Great China'}
限制屬性的添加
python中添加一個(gè)屬性很靈活白热,但有時(shí)候作為類的創(chuàng)建者,并不希望類的使用者在對類添加額外的屬性粗卜,或者對添加的屬性進(jìn)行限制屋确,這種情況下我們只需要對slots列表賦值即可:
- 如果slots賦值成空列表,那么就不允許在類的內(nèi)部或者外部添加任何屬性续扔。
- 如果允許添加特定名字的屬性攻臀,那只需要把這些名字存放在slots中。
需要注意的是:
- slots只能對實(shí)例屬性起限制的作用
- 定義了slots以后纱昧,實(shí)例屬性的讀取就不再通過obj.dict來獲取
- 如果類屬性與slots中的變量同名刨啸,則該類屬性被設(shè)置為readonly,并且會覆蓋同名的實(shí)例屬性
class ChinesePeople:
# Class Attribute
country = 'China'
def __init__(self, name):
# Instance Attribute
self.name = name
__slots__ = ['name']
p1.age = 18 # AttributeError: 'ChinesePeople' object has no attribute 'age'
屬性的訪問權(quán)限
與C#, Java不一樣识脆,python中并沒有像private/protect/public這樣的關(guān)鍵字來修飾屬性活著方法的訪問權(quán)限设联。那在python中如何實(shí)現(xiàn)屬性的私有化和只讀?
在python中灼捂,如果一個(gè)屬性是以雙下劃線開頭(例如__name)离例,那么這屬性就是私有的(注意,這里要和類內(nèi)置的屬性區(qū)分開悉稠,例如前面提到的dict)宫蛆。來看看下面的代碼:
class ChinesePeople:
# Class Attribute
country = 'China'
__province = 'Taiwan'
def __init__(self, name):
# Instance Attribute
self.name = name
self.__salary = 5000
p1 = ChinesePeople('Bill')
print(ChinesePeople.__province) # AttributeError: type object 'ChinesePeople' has no attribute '__province'
print(p1.__salary) # AttributeError: 'ChinesePeople' object has no attribute '__salary'
下面,我們先分別來看看class.dict和obj.dict的值偎球,然后再來談?wù)刾ython中的私有化屬性洒扎。
print(p1.__dict__)
print(ChinesePeople.__dict__)
#### Outputs ###
{'name': 'Bill', '_ChinesePeople__salary': 5000}
{'__module__': '__main__', 'country': 'China', '_ChinesePeople__province': 'Taiwan', '__init__': <function ChinesePeople.__init__ at 0x1073e3a60>, '__dict__': <attribute '__dict__' of 'ChinesePeople' objects>, '__weakref__': <attribute '__weakref__' of 'ChinesePeople' objects>, '__doc__': None}
通過上面代碼的輸出,我們一定會產(chǎn)生一個(gè)疑問衰絮,_ChinesePeople__salary和_ChinesePeople__province是什么鬼,我們明明沒有定義這個(gè)兩個(gè)屬性磷醋,我們定義的是__salary和__province猫牡,這是怎么回事?
其實(shí)邓线,python中沒有私有化屬性的概念淌友,上面提到的私有化煌恢,實(shí)際上是"偽私有化"。當(dāng)python解釋器遇到以雙下劃線開頭的屬性震庭,那么會對這個(gè)屬性進(jìn)行重命名瑰抵,也就是在這個(gè)屬性前面添加classname_。例如器联,將__salary重命名為_ChinesePeople__salary二汛。這樣,當(dāng)我們通過__xxxx訪問一個(gè)屬性的時(shí)候:
- 如果是在類內(nèi)部訪問拨拓,那么解釋也會做相應(yīng)的轉(zhuǎn)化肴颊。也就是說,訪問obj.__xxxx/class.__xxxx時(shí)渣磷,會轉(zhuǎn)換成訪問obj._classname__xxxx/class.__classname__婿着。很顯然,被重命名過的屬性醋界,是可以在dict中找到竟宋。
- 如果是在類外部訪問,那么解釋器就不做轉(zhuǎn)化形纺,所以也就不能從dict中找到__xxxx袜硫。
對于屬性的只讀限制,這里先提供一下實(shí)現(xiàn)的方式挡篓,由于涉及到裝飾器和描述器的概念婉陷,后面將會有特定的筆記來說明。實(shí)現(xiàn)只讀屬性官研,大概的思路有下面三種:
- 將屬性設(shè)置為私有屬性秽澳,而后通過get方法進(jìn)行讀取
- 通過@property這個(gè)裝飾器來實(shí)現(xiàn)
- 通過描述器來實(shí)現(xiàn)
方法
方法和函數(shù)
比起屬性和變量,方法和函數(shù)好像更復(fù)雜一點(diǎn)戏羽。從本質(zhì)上講担神,方法和函數(shù)都是可調(diào)用的對象,它們的區(qū)別在于:
- 函數(shù)(function):沒有隱式的參數(shù)始花。簡單點(diǎn)說妄讯,如果一個(gè)函數(shù)需要傳遞進(jìn)去兩個(gè)參數(shù),那么調(diào)用者必須傳遞兩個(gè)參數(shù)酷宵,解釋器不會幫忙隱式傳遞
- 方法(method):方法的第一個(gè)參數(shù)是self或者cls亥贸,分別表示類的實(shí)例對象和類對象。對于調(diào)用者而言浇垦,第一個(gè)不需要顯示傳遞炕置,因?yàn)榻忉屍饕呀?jīng)幫忙傳遞了第一個(gè)參數(shù)
來看看下面的代碼:
def hello(self, name):
print(self,',', name)
class ChinesePeople:
# Class Attribute
country = 'China'
def __init__(self, name):
# Instance Attribute
self.name = name
def say_hi(self, name):
print(self,',', name)
p1 = ChinesePeople('Bill')
print(hello, p1.say_hi)
hello(1000, 'Jim')
p1.say_hi('Jim')
#### Outputs ###
<function hello at 0x00000245A3FC6D08> <bound method ChinesePeople.say_hi of <__main__.ChinesePeople object at 0x00000245A3FF6E48>>
1000 , Jim
<__main__.ChinesePeople object at 0x00000245A3FF6E48> , Jim
上面的代碼中,hello就是一個(gè)function,而類中定義的say_hi則是一個(gè)method朴摊。通過實(shí)例對象調(diào)用say_hi的時(shí)候默垄,解釋器幫忙傳遞了self,因此我們只需要顯示的傳遞一個(gè)參數(shù)甚纲。(其實(shí)這里參數(shù)的名字定義為self口锭,只是為了好理解,其他的名字也是沒有問題介杆,但建議使用self)
實(shí)例方法鹃操,類方法和靜態(tài)方法
上面提到了方法和函數(shù)的差別在于解釋器是否幫忙傳遞第一個(gè)參數(shù)。那根據(jù)解釋器傳遞的第一個(gè)參數(shù)的值的不同这溅,又分為下面三種方法:
- 實(shí)例方法:第一個(gè)參數(shù)傳遞的是實(shí)例對象
- 類方法:第一個(gè)參數(shù)傳遞的是類對象
- 靜態(tài)方法:不存在隱式傳遞的第一個(gè)參數(shù) (這個(gè)其實(shí)就等價(jià)于函數(shù)了)
以上三種方法组民,都是需要通過各自的裝飾器來定義,如下面的代碼:
def hello(self, name):
print(self,',', name)
class ChinesePeople:
# Class Attribute
country = 'China'
def __init__(self, name):
# Instance Attribute
self.name = name
def say_hi(self, name):
print(self,',', name)
@classmethod
def class_say_hi(cls, name):
print(cls,',', name)
@staticmethod
def static_say_hi(name):
print(name)
p1 = ChinesePeople('Bill')
print(p1.say_hi, ChinesePeople.class_say_hi, ChinesePeople.static_say_hi)
print(p1.say_hi, p1.class_say_hi, p1.static_say_hi)
p1.say_hi('Jim')
p1.class_say_hi('Jim')
p1.static_say_hi('Jim')
ChinesePeople.class_say_hi('Jim')
ChinesePeople.static_say_hi('Jim')
#### Outputs ###
<bound method ChinesePeople.say_hi of <__main__.ChinesePeople object at 0x00000245A4036710>> <bound method ChinesePeople.class_say_hi of <class '__main__.ChinesePeople'>> <function ChinesePeople.static_say_hi at 0x00000245A403F510>
<bound method ChinesePeople.say_hi of <__main__.ChinesePeople object at 0x00000245A4036710>> <bound method ChinesePeople.class_say_hi of <class '__main__.ChinesePeople'>> <function ChinesePeople.static_say_hi at 0x00000245A403F510>
<__main__.ChinesePeople object at 0x00000245A4036710> , Jim
<class '__main__.ChinesePeople'> , Jim
Jim
<class '__main__.ChinesePeople'> , Jim
Jim
從上面代碼可知悲靴,不論是實(shí)例方法臭胜、類方法和靜態(tài)方法,都是可以通過實(shí)例對象來訪問癞尚,并且解釋器都是會正確的傳遞一個(gè)參數(shù)耸三;但是對于類對象而言,是無法調(diào)用實(shí)例方法浇揩。
動態(tài)添加方法
上文屬性的部分仪壮,我們了解到屬性是可以在類外部動態(tài)來添加。那對于方法而言胳徽,同樣的方式是否適用积锅?
def hello(self, name):
print(self,',', name)
def hello1(self, name):
print(self,',', name)
class ChinesePeople:
# Class Attribute
country = 'China'
def __init__(self, name):
# Instance Attribute
self.name = name
def say_hi(self, name):
print(self, ',', name)
p1 = ChinesePeople('Bill')
p1.hello = hello
ChinesePeople.hello1 = hello1
print(hello, p1.hello)
print(p1.hello1, ChinesePeople.hello1)
print(p1.say_hi, ChinesePeople.say_hi)
#### Outputs ###
<function hello at 0x00000245A40702F0> <function hello at 0x00000245A40702F0>
<bound method hello1 of <__main__.ChinesePeople object at 0x00000245A406A0F0>> <function hello1 at 0x00000245A40701E0>
<bound method ChinesePeople.say_hi of <__main__.ChinesePeople object at 0x00000245A406A0F0>> <function ChinesePeople.say_hi at 0x00000245A4070378>
從上面的輸出,可以得出以下結(jié)論:
- 如果直接在實(shí)例對象上通過一個(gè)函數(shù)來賦值养盗,那么這個(gè)函數(shù)不會轉(zhuǎn)化成實(shí)例方法
- 直接在類對象上通過一個(gè)函數(shù)來賦值缚陷,接著通過實(shí)例對象來調(diào)用,那么相當(dāng)于是實(shí)例方法(類似于在類內(nèi)部定義了一個(gè)實(shí)例方法)往核。
對于類方法和靜態(tài)方法的添加箫爷,也是類似,前提是要在相應(yīng)的方法上加上@classmethod和@staticmethod裝飾器即可聂儒。
思考一個(gè)問題虎锚,在上述代碼基礎(chǔ)上,再創(chuàng)建一個(gè)p2實(shí)例衩婚,而后分別調(diào)用hello和hello1窜护,是什么結(jié)果?(自己動手谅猾,豐衣足食)柄慰。
上面的添加的實(shí)例方法鳍悠,是作用到類的所有實(shí)例中税娜。如果我們只想對特定的實(shí)例添加方法坐搔,可以通過types.MethodType把一個(gè)函數(shù)綁定到特定的實(shí)例上:
import types
def hello1(self, name):
print(self,',', name)
class ChinesePeople:
pass
p1 = ChinesePeople()
p2 = ChinesePeople()
p1.hello1 = types.MethodType(hello1, p1)
p1.hello1('name') # ok
p2.hello1('name') # AttributeError: 'ChinesePeople' object has no attribute 'hello1'
方法的添加限制和私有化
方法的添加限制和私有化與屬性類似,即通過slots限制敬矩、通過雙下劃線開頭實(shí)現(xiàn)私有化概行。這里就不再贅述。