Python 3 學習筆記之類與例項

蔣志碧發表於2018-07-23

一、定義

1.1、定義

類 (class) 封裝一組相關資料,使之成為一個整體,並使用一種方法持續展示和維護。

這有點像把零件組裝成整車提供給使用者,無須瞭解汽車的內部結構和工作原理,只要知道方向盤,剎車和油門這些外部介面就可以正常行駛。

類存在兩種關係
  1. 繼承(inheritance,is-a)自某個族類

    繼承可以用來表達本車屬於某廠的哪個車族系列,除了繼承原車系的技術和優勢,還可基於某些環境進行改進。

  2. 組合(composition,has-a)了哪些部件

    組合可用來表述該車使用了哪些零部件,比如最新的發動機。

類與模組的不同之處
  1. 類可生成多個例項。
  2. 類可被繼承和擴充套件。
  3. 類例項的生命週期可控。
  4. 類支援運算子,可按需過載。

這些特性模組沒有或者不需要,同時,模組粒度大,模組可用來提供遊戲場景級別的解決方案,而類則是該場景下的特定家族和演員。

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
複製程式碼

函式內定義的型別物件,在所有例項死亡後,會被垃圾回收。

型別物件除了用來建立例項,也為所有例項定義了基本操作介面,其負責管理整個家族的可共享資料和行為目標。

例項只儲存私有特徵,其以內部引用從所屬型別或其它所屬祖先類查詢所需的方法,用來驅動展現個體面貌。

Python 3 學習筆記之類與例項

名字空間

型別有自己的名字空間,儲存當前型別定義的欄位和方法。這其中並不包括所繼承的祖先成員,其同樣以引用關聯祖先型別,無須複製到本地。

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
複製程式碼

Python 3 學習筆記之類與例項

例項 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'
'''
複製程式碼

特殊方法

下面又直譯器自動呼叫,與物件生命週期相關的方法。

  1. __ new __:構造方法,建立物件例項
  2. __ init __:初始化方法,設定例項的相關屬性
  3. __ 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 夢工廠】

Python 3 學習筆記之類與例項

相關文章