一、定義
1.1、定義
類 (class) 封裝一組相關資料,使之成為一個整體,並使用一種方法持續展示和維護。
這有點像把零件組裝成整車提供給使用者,無須瞭解汽車的內部結構和工作原理,只要知道方向盤,剎車和油門這些外部介面就可以正常行駛。
類存在兩種關係
-
繼承(inheritance,is-a)自某個族類
繼承可以用來表達本車屬於某廠的哪個車族系列,除了繼承原車系的技術和優勢,還可基於某些環境進行改進。
-
組合(composition,has-a)了哪些部件
組合可用來表述該車使用了哪些零部件,比如最新的發動機。
類與模組的不同之處
- 類可生成多個例項。
- 類可被繼承和擴充套件。
- 類例項的生命週期可控。
- 類支援運算子,可按需過載。
這些特性模組沒有或者不需要,同時,模組粒度大,模組可用來提供遊戲場景級別的解決方案,而類則是該場景下的特定家族和演員。
1.2、建立
定義類,以此為個體為例。關鍵字 class 同樣是執行期指令,用於完成型別物件的建立。
class User:
pass
複製程式碼
可在函式內定義,以限制其作用範圍。
型別與例項
如果類在模組中定義,那麼其生命週期與模組等同,如果被放在函式內,那麼每次都是新建。即便名字和內容相同,也屬於不同型別。
def test():
class X:
pass
return X()
>>> a,b = test(),test()
>>> a.__class__ is b.__class__
Out[1]: False
複製程式碼
函式內定義的型別物件,在所有例項死亡後,會被垃圾回收。
型別物件除了用來建立例項,也為所有例項定義了基本操作介面,其負責管理整個家族的可共享資料和行為目標。
例項只儲存私有特徵,其以內部引用從所屬型別或其它所屬祖先類查詢所需的方法,用來驅動展現個體面貌。
名字空間
型別有自己的名字空間,儲存當前型別定義的欄位和方法。這其中並不包括所繼承的祖先成員,其同樣以引用關聯祖先型別,無須複製到本地。
class A:
a = 100 #類欄位
def __init__(self, x): #例項初始化方法
self.x = x #例項欄位
def_get_x(self): #例項方法
return self.x
class B(A): #繼承自A
b = "hello"
def __init__(self, x, y):
super().__init__(self,x) #呼叫父類的初始化方法
self.y = y
def get_y(self):
return self.y
複製程式碼
例項 instance o 會儲存所有繼承層次的例項欄位,因為這些都屬於其私有資料。
>>> o = B(1,2)
>>> print(A.__dict__)
{
'a': 100,
'__init__': <function A.__init__ at 0x109d04f28>,
'_get_x': <function A._get_x at 0x109d046a8>,
...
}
>>> print(B.__dict__)
{
'b': 'hello',
'__init__': <function B.__init__ at 0x109d272f0>,
'get_y': <function B.get_y at 0x109d27378>,
...
}
複製程式碼
當通過例項或類訪問某個成員時,會從當前物件開始(instance o 開始查詢),依次由近到遠向祖先類查詢(即 o --> class B --> class A 進行成員查詢)。
如此做的好處就是祖先類的新增功能可以直接 【廣播】給所有後代。
在繼承層次的不同名字空間中允許有同名成員,並按順序優先命中。
二、欄位
依照所處空間不同,我們將欄位分為型別欄位和例項欄位。
官方將成員統稱為 Attribute,我們可按例將資料當做欄位。
2.1、型別欄位
【型別欄位】在 class 語句塊內直接定義,而例項欄位必須通過**例項引用(self)**賦值定義。
僅從執行方式來看,無論例項方法存在於哪級型別,其隱式引數 self 總指向當前呼叫例項。
class A:
def test(self): #self 總是指向引用的當前例項,這與繼承層次無關
print(self)
self.x = 100
class B(A):
pass
if __name__=="__main__":
o = B()
print(hex(id(o)))
print(o.test())
0x109d2f358
<__main__.B object at 0x109d2f358>
複製程式碼
例項引數 self 只是約定成俗的名字,這類似於其它語言中的 this,換成 this 同樣有效。
2.2、欄位賦值
可使用賦值語句為型別或例項新增的新欄位。
class A:
pass
if __name__=="__main__":
A.x = 100 #新增型別欄位
print(A.x)
print(vars(A))
'''
100
{x': 100, ...}
'''
複製程式碼
可一旦例項重新賦值,就將會在其名字空間建立同名欄位,並會遮蔽原欄位。
a = A()
a.b = 200
print(a.b)
print(vars(a))
'''
200
{'b': 200}
'''
複製程式碼
2.3、私有欄位
將私有欄位暴露給使用者是很危險的。因為無論是修改還是刪除都無法截獲,由此可能引發意外錯誤。因為語言沒有嚴格意義上的訪問許可權設定,所以只好將它們隱藏起來。
如果成員名字以雙下劃線開頭,但沒有以雙下劃線結尾,那麼編譯器會自動對其重新命名。
class X:
__table = "user" #型別變數
def __init__(self,name):
self.name = name #例項變數
def get_name(self):
return self.name
複製程式碼
同時雙下劃線開頭課結尾的,通常是系統方法,比如 __ init __ ,__ hash __ ,__ main __等。
所謂重新命名,就是編譯器附加了型別名稱字首。雖然這種做法不能真正阻止使用者訪問,但基於名字的約定也算一種提示。這種方式讓繼承類也無法訪問。
重新命名機制總是針對當前型別,繼承型別無法訪問重新命名後的基類成員。
可將雙下劃線字首改為單下劃線,這樣雖然不能自動重新命名,不過提示作用依舊。
class A:
__name = "user" #雙下劃線成員
class B(A):
def test(self):
print(self.__name)
>>> B().test()
'''
AttributeError: 'B' object has no attribute '_B__name'
'''
class A:
_name = "user" #單下劃線成員
class B(A):
def test(self):
print(self._name)
>>> B().test()
'''
user
'''
複製程式碼
三、屬性
對私有欄位會進行重新命名保護,那公開欄位如何處理呢?
問題是核心在於訪問攔截,必須由內部邏輯決定如何返回結果。而屬性(property)機制就是將讀、寫和刪除操作對映到指定的方法呼叫上,從而實現操作控制。
class C:
def __init__(self, name):
self.__name = name
@property
def name(self): #讀
return self.__name
@name.setter
def name(self, value): #寫
self.__name = value
@name.deleter
def name(self): #刪除
raise AttributeError("can't delete attribute")
c = C("user")
print(c.name)
# user
c.name = "abc"
print(c.name)
# abc
del c.name
print(c.name)
#can't delete attribute
複製程式碼
這種 @ 語法被稱作裝飾器(decorator)。
多個方法名必須相同,預設從讀方法尅是定義屬性,隨後以屬性名定義寫和刪除。
如果實現只讀,或禁止刪除,則只需去掉對應的方法即可。
四、方法
方法是一種特殊函式,其與特定物件繫結,用來獲取或修改物件狀態。
實際上,無論是物件構造,初始化,析構還是運算子,都以方法實現。根據繫結目標和呼叫方法的不同,方法可分為例項方法,型別方法,以及靜態方法。
名字以上下劃線開始和結束的方法,通常有特殊用途,其由直譯器和內部機制呼叫。
例項方法
例項方法與例項物件繫結,在其引數列表中,將繫結物件作為第一引數,以便在方法中讀取或修改資料狀態。在以例項引用呼叫方法時,無須顯式傳入第一實參,而由直譯器自動完成。
官方建議引數名用 self,同樣以 cls 作為型別方法的第一引數名。
class W:
def __init__(self, name):
self.__name = name
def get(self): #以例項引用呼叫,自動傳入 self 引數
return self.__name
def set(self, value):
self.__name = value
>>> w = W("python")
>>> w.get() #忽略第一引數
'''
python
'''
複製程式碼
型別方法
型別方法用來維護型別狀態,面向族群提供服務介面。除繫結的第一引數名稱不同外,還需新增專門的裝飾器,以便直譯器將其例項方法區分開來。
class D:
__data = "D.data"
@classmethod #定義為型別方法
def get(cls): #直譯器自動將型別物件 D 作為 cls 引數傳入
return cls.__data
@classmethod
def set(cls, value):
cls.__data = value
>>> D.get() #同樣忽略 cls 引數
'''
D.data
'''
複製程式碼
靜態方法
靜態方法,則更像是普通函式。其既不接收例項引用,也不參與型別處理,所以就沒有自動傳入第一引數。使用靜態方法,更多原因是將型別作為一個作用域,或者當前型別新增便捷介面。
class DES:
def __init__(self, key):
self.__key = key
def encrypt_bytes(self, value):
return str(self.__key) + str(value)
@staticmethod
def encrypt(key, s):
return DES(key).encrypt_bytes(s.encode("utf-8"))
>>> DES.encrypt("key", "value")
'''
keyb'value'
'''
複製程式碼
特殊方法
下面又直譯器自動呼叫,與物件生命週期相關的方法。
- __ new __:構造方法,建立物件例項
- __ init __:初始化方法,設定例項的相關屬性
- __ del __:析構方法,例項被回收時呼叫
建立例項時,會先呼叫析構方法和初始化方法。
class E:
def __new__(cls, *args): #與__init__接收相同的呼叫函式
print("__new__", args)
return super().__new__(cls)
def __init__(self, *args): ##self 由 __new__建立並返回
print("__init__", args)
>>> E(1,2)
'''
__new__ (1, 2)
__init__ (1, 2)
'''
複製程式碼
如果 __ new __ 返回例項與 cls 型別不符,將導致 __ init __ 無法執行。
五、總結
學習到此,我總算把類的建立,屬性和方法等弄清楚了,我最想強調一點,希望讀者把 例項 self 引數弄明白,後續編碼過程中使用較多。
還要清楚例項方法和靜態方法的區別。
下一節將詳細介紹類的繼承及過載。
如果覺得文章對你有幫助,歡迎關注個人公號 【Python 夢工廠】