python粗談面向對象(二)

  • 2020 年 1 月 17 日
  • 筆記

淺談super()

super並不是一個函數,是一個類名,形如super(B, self)事實上調用了super類的初始化函數,產生了一個super對象;Python的多繼承類是通過mro的方式來保證各個父類的函數被逐一調用,而且保證每個父類函數只調用一次(如果每個類都使用super),並且按照mro序列一次調用。下面是一個小練習:

class A:      def fun(self):          print('in A')  class B(A):      def fun(self):          super().fun()          print('in B')  class C(A):      def fun(self):          print('in C')  class D(B,C):      def fun(self):          super().fun()          print('in D')  print(D.mro())  # 列印mro序列  obj = D()  obj.fun()    # 列印內容如下  [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]  in C  in B  in D

從列印結果我們可以知道super並不是簡單的按照調用父類那麼簡單。它是按照mro序列的排序方式調用的。在D類fun中super指向B類,在B類的fun方法中super指向下一個mro序列也就是C類。所以最終列印順序是C類的fun B類的fun最後是D類的fun。

在看一個簡單的示例:

class A:      def fun(self):          print('in A')  class B(A):      def fun(self):          super().fun()          print('in B')  class C(A):      def fun(self):          print('in C')  class D(B,C):      def fun(self):          super(B,self).fun()  # 跳過B類,使用下一個mro序列          print('in D')  print(D.mro())  # 列印mro序列  obj = D()  obj.fun()    # 列印內容如下  [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]  in C  in D

super(B,self).fun()函數是表示跳過B類。使用下一個mro序列也就是C類。

面向對象之類成員

類的成員分為兩種形式:

  • 公有成員:在任何地方都可以訪問。
  • 私有成員:只能在類的內部訪問。

類屬性

類的靜態欄位(靜態屬性):

  • 公有靜態欄位:類可以訪問,類內部可以訪問,派生類中可以訪問。
  • 私有靜態欄位:僅類內部可以訪問。

訪問類的公有欄位:

class A:      name = "公有靜態欄位"      def func(self):          print(A.name)  class B(A):      def show(self):          print(B.name)  print(A.name)   # 類訪問  obj_A = A()  obj_A.func()     # 類內部訪問  obj_B = B()  obj_B.show() # 派生類中訪問

訪問類的私有欄位:

class A:      __name = "私有靜態欄位"      def func(self):          print(A.__name)  class B(A):      def show(self):          print(A.__name)  # print(A.__name)     # 不可以在類外訪問  obj_A = A()  obj_A.func()     # 可以在類內部訪問  # obj_B = B()  # obj_B.show() # 不可以在派生類中訪問

對象屬性

  • 公有普通欄位:對象可以訪問;類內部可以訪問;派生類中可以訪問
  • 私有普通欄位:僅類內部可以訪問;

訪問對象公有欄位:

class A:      def __init__(self):          self.foo = "對象公有欄位"      def func(self):          print(self.foo) # 類內部訪問  class B(A):      def show(self):          print(self.foo) # 子類中訪問父類對象的公有欄位  obj = A()  print(obj.foo)    # 通過對象訪問  obj.func()  # 類內部訪問    obj_B = B()  obj_B.show()  # 子類中訪問父類對象的公有欄位    # 列印內容如下  對象公有欄位  對象公有欄位  對象公有欄位

訪問對象私有屬性:

class A:      def __init__(self):          self.__foo = "對象私有欄位"      def func(self):          print(self.__foo) # 類內部訪問  obj = A()  obj.func()  # 類內部訪問    #列印內容如下  對象私有欄位

類方法

  • 公有方法:對象可以訪問,類內部可以訪問,派生類中可以訪問。
  • 私有方法:僅類內部可以訪問。

訪問公有方法:

class A:      def fun(self):          print("公有方法 A  fun")  class B(A):      def show(self):          print("公有方法 B show")      def func(self):          self.show()  obj = B()  obj.show()  # 通過對象訪問  obj.func()  # 類內部訪問  obj.fun()  # 子類中訪問父類方法    # 列印內容如下  公有方法 B show  公有方法 B show  公有方法 A  fun

在類中訪問私有方法:

class A:      def __show(self):          print("私有方法 A show")      def func(self):          self.__show()  obj = A()  obj.func()  # 類內部訪問    # 列印內容如下  私有方法 A show

總結:

對於這些私有成員來說,他們只能在類的內部使用,不能再類的外部以及派生類中使用。

如果非要訪問私有成員的話,我們可以通過類名.__dict__查看類的所有屬性和方法。如下圖所示:

由上圖我們可以看出私有方法只不過是Python在前面加了_類名__方法的方式進行了簡單的加密。所以雖然是私有方法或者私有屬性,我們還是可以用對象或者類在類的外部進行調用。但既然我們把它定義成私有屬性,就表示我們只想在類的內部調用而不打算在類的外部調用。所以沒有必要定義了私有屬性又在外部調用。

關於類的方法從類型上分為以下幾種:

實例方法:從名字上看就可以知道主要是給實例對象調用的,第一個參數必須是實例對象,這也應該沒什麼異議畢竟是給實例使用的,參數名一般約定俗成為「self」,如果你看它不順眼也可以改成自己喜歡的。通過它來傳遞實例的屬性和方法。主要由實例對象調用,雖然類也可以調用,但一般不建議。

類方法: 從名字上也可以看出它主要是給類使用的,使用裝飾器@classmethod。第一個參數必須是當前類,該參數名一般約定為「cls」,一樣如果你不習慣cls可以改成自己喜歡的,通過它來傳遞類的屬性和方法,主要由類調用,雖然實例對象也可以調用,但一般不建議。

靜態方法:這是一個特殊的方法,它除了在類空間內創建了一個函數外,和類沒有任何關係,使用裝飾器@staticmethod。參數隨意,沒有「self」和「cls」參數這些俗套的東東,如果想要在靜態方法中調用類的成員或者對象的成員需要將類或者對象傳遞給靜態方法。實例對象和類對象都可以調用。

雙下方法:這也是個特殊方法,他是解釋器提供的由雙下劃線加方法名加雙下劃線 __方法名__的具有特殊意義的方法,雙下方法主要是python源碼程式設計師使用的,我們在開發中盡量不要使用雙下方法,但是深入研究雙下方法,有益於我們閱讀源碼。

實例方法:

class A:      name = "xiao ming"      def fun(self):     # 實例方法          self.__fun2()  # 調用私有實例方法      def __fun2(self):  # 私有實例方法          print("我是私有的實例方法")      def fun3(self):          print("我是公有的實例方法")  obj = A()  obj.fun()  A.fun3("我必須傳個參數")  A.fun("我必須傳個參數")    # 列印內容如下  我是私有的實例方法  我是公有的實例方法  AttributeError: 'str' object has no attribute '_A__fun2'

類雖然可以調用實例方法,但是必須要傳遞個參數給實例方法,如果實例方法中在調用其它的實例方法,無論調用的是公有實例方法還是私有實例方法都會出現問題,因為這些方法需要參數,通過類的方式無法傳遞參數所以會報錯。下面是圖片應該看的更清晰些,所以說實例方法就是給實例用的,類就不要增加存在感了。如果有特殊情況需要類參與那就使用類方法。不要和實例方法混在一起。

實例對象在調用方法時就不需要傳遞參數,這是因為Python為我們隱式的把實例對象空間地址傳給了實例方法,所以實例對象在調用實例方法時不會報錯,因為Python已經為我們將參數隱式的傳遞給了實例方法。只是我們沒看到,所以說眼睛看到的未必就是真實的。

類方法:

class A:      name = "xiao ming"      @classmethod      def fun(cls):     # 類方法          cls.__fun2()  # 調用私有類方法          print(cls)    # 列印cls記憶體地址      @classmethod      def __fun2(cls):  # 類私有方法          print("我是類的私有方法")      @classmethod      def fun3(cls):          print("我是公有的實例方法")  obj = A()  obj.fun()  # 對象調用類方法  A.fun3()   # 類調用類方法  A.fun()    # 類調用類方法  print(A)    # 列印內容如下  我是類的私有方法  <class '__main__.A'>  # 通過實例對象調用的類方法  我是公有的實例方法  我是類的私有方法  <class '__main__.A'>  <class '__main__.A'>

從列印結果我們可以知道,類的實例對象也可以正常調用類方法,並且Python為我們將類A隱式的傳遞給了類方法,而不是將實例對象空間傳遞給了類方法。所以我們不能在類方法中使用對象的屬性和方法,除非我們將實例對象空間傳遞給類方法,這就需要在定義類方法時,給類方法在加個形參,然後使用實例對象顯式的將對象空間傳遞給類方法。所以說既然實例對象有自己的實例方法就不要和類方法湊熱鬧了,這就是不建議用實例對象調用類方法的原因。

小示例:統計創建實例對象的個數。

class A:      count = 0      def __init__(self):          A.obj_count()  # 統計創建了多少個實例對象      @classmethod      def obj_count(cls):          cls.count += 1  obj_1 = A()  obj_2 = A()  obj_3 = A()  print(A.count)    # 列印內容如下  3

靜態方法:

class A:     @staticmethod     def fun():         print("我是靜態函數")  obj = A()  obj.fun()  A.fun()

關於類的靜態方法沒什麼好說的,就是在類空間內創建了一個與類不發生任何關係的函數,也不能說一點關係沒有,畢竟是在類空間創建的。類和實例化對象都可以正常調用。

雙下方法:

我們知道在Python中一切皆是對象,而我們又知道對象是類實例化出來的,所以Python中的對象必然都是通過某個具體類實例化出來的。例如:

我們可以知道str_1是str類的實例化對象,所以str_1可以使用str類中的所有方法,而str類繼承object類所以str_1也可以使用object類中的方法。如果我們想要獲取字元串的長度可以直接使用len(字元串),這是為什麼呢?,那麼len又屬於哪個類的方法呢?我們做個簡單的示例:

class A:      def __len__(self):          print("計算長度")  obj = A()  len(obj)

下面是圖片,應該更好理解:

好的既然不能解釋,那麼我們就讓它能解釋。

class A:      def __len__(self):          return 4  obj = A()  print(len(obj))    # 列印內容如下  4

我們可以發現這回沒有報錯了,那如果我在類A中在定義一個用於統計字元串長度的函數__len__,是不是類對象在統計屬性長度時就可以調用本類中len功能了呢?答案是理論上可以,你可以單獨創建個數據類型然後不繼承object類,繼承你寫的類這樣就可以調用你寫的len了。

關於雙下劃線方法我們要知道幾個主要的如下:

__new__:在實例化對象時為對象開闢記憶體空間。

class A:      def __init__(self):          self.x = 1          print('in __init__')  obj = A()  print(obj.x)     # 列印內容如下  in __init__  1

下面演示一個沒開闢空間的實例化對象:

class A:      def __init__(self):          self.x = 1  # 為實例對象封裝屬性          print('in __init__')      def __new__(cls, *args, **kwargs):          print("in __new__")  obj = A()  obj.name = "Hello World"  # 為實例對象封裝屬性  print(obj.x)

列印如下圖所示:

觸發了__new__後並沒有執行__init__函數,所以也就沒有給obj對象封裝x這個屬性,當調用obj.x這個屬性時,找不到也是自然,但是給對象封裝name屬性時也失敗,究其原因就是實例對象在記憶體中沒有空間,所以無法為其封裝屬性。

下面我們在類A中的__new__中調用object的__new__為對象開闢記憶體。

class A:      def __init__(self):          self.x = 1  # 為實例對象封裝屬性          print('in __init__')      def __new__(cls, *args, **kwargs):          print("in __new__")          return object.__new__(cls)  # 調用object的__new__為對象開闢空間  obj = A()  obj.name = "Hello World"  # 為實例對象封裝屬性  print(obj.x)

列印內容如下:

下面我們來演示個單實例的程式碼:什麼是單實例?單實例就是類創建N個對象,但是這N個對象都使用一塊記憶體空間。

class A:      instance_flag = None  # 如果有創建一個實例對象就將空間賦值給instance_flag      def __new__(cls, *args, **kwargs):          if not cls.instance_flag:              cls.instance_flag = object.__new__(cls)          return cls.instance_flag  obj_1 = A()  obj_2 = A()  obj_3 = A()  print(obj_1)  print(obj_2)  print(obj_3)    # 列印內容如下  <__main__.A object at 0x0000000002868588>  <__main__.A object at 0x0000000002868588>  <__main__.A object at 0x0000000002868588>

單例模式是一種常用的軟體設計模式。在它的核心結構中只包含一個被稱為單例類的特殊類。通過單例模式可以保證系統中一個類只有一個實例而且該實例易於外界訪問,從而方便對實例個數的控制並節約系統資源。如果希望在系統中某個類的對象只能存在一個,單例模式是最好的解決方案。

【採用單例模式動機、原因】

對於系統中的某些類來說,只有一個實例很重要,例如,一個系統中可以存在多個列印任務,但是只能有一個正在工作的任務;一個系統只能有一個窗口管理器或文件系統;一個系統只能有一個計時工具或ID(序號)生成器。如在Windows中就只能打開一個任務管理器。如果不使用機制對窗口對象進行唯一化,將彈出多個窗口,如果這些窗口顯示的內容完全一致,則是重複對象,浪費記憶體資源;如果這些窗口顯示的內容不一致,則意味著在某一瞬間系統有多個狀態,與實際不符,也會給用戶帶來誤解,不知道哪一個才是真實的狀態。因此有時確保系統中某個對象的唯一性即一個類只能有一個實例非常重要。

如何保證一個類只有一個實例並且這個實例易於被訪問呢?定義一個全局變數可以確保對象隨時都可以被訪問,但不能防止我們實例化多個對象。一個更好的解決辦法是讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例被創建,並且它可以提供一個訪問該實例的方法。這就是單例模式的模式動機。

【單例模式優缺點】

【優點】

一、實例控制

單例模式會阻止其他對象實例化其自己的單例對象的副本,從而確保所有對象都訪問唯一實例。

二、靈活性

因為類控制了實例化過程,所以類可以靈活更改實例化過程。

【缺點】

一、開銷

雖然數量很少,但如果每次對象請求引用時都要檢查是否存在類的實例,將仍然需要一些開銷。可以通過使用靜態初始化解決此問題。

二、可能的開發混淆

使用單例對象(尤其在類庫中定義的對象)時,開發人員必須記住自己不能使用new關鍵字實例化對象。因為可能無法訪問庫源程式碼,因此應用程式開發人員可能會意外發現自己無法直接實例化此類。

__call__:實例化對象()  或者 類名()()會觸發

class A:      def __init__(self):          pass      def __call__(self, *args, **kwargs):          print('__call__')    obj = A()  # 實例化對象  obj()    # 執行 __call__  A()()  # 執行 __call__  __call__  __call__

__item__:當以字典的形式操作實例對象時會觸發。

class A:      def __init__(self,name):          self.name=name        def __getitem__(self, item):          print(self.__dict__[item])        def __setitem__(self, key, value):          print("添加屬性,修改屬性時,激活我")          self.__dict__[key]=value      def __delitem__(self, key):          print('del obj[key]刪除值時,我執行')          self.__dict__.pop(key)      def __delattr__(self, item):          print('刪除屬性時,我執行')          self.__dict__.pop(item)    obj_1=A('小明')  obj_1['age']=18         # 新增屬性  obj_1['age1']=19        # 修改屬性  del obj_1.age1          # 刪除屬性  del obj_1['age']        # 刪除字典的值  print(obj_1.__dict__)  添加屬性,修改屬性時,激活我  添加屬性,修改屬性時,激活我  刪除屬性時,我執行  del obj[key]刪除值時,我執行  {'name': '小明'}

好了雙下方法就到這裡吧。

property:

將一個類的函數定義成屬性,對象再去使用的時候,可以直接使用對象.屬性的方式來執行這個函數,從表面無法判斷是屬性還是方法。

class A:      @property      def fun(self):          print("我是被分裝成屬性的函數")  obj = A()  obj.fun   # 調用屬性    # 列印內容如下  我是被分裝成屬性的函數

如果單從調用fun來看根本看不出來fun到底是函數還是一個真正的屬性。

那麼我們對property都有哪些操作呢?可以獲取property,設置property和刪除property三種操作。這三種操作有兩種實現方式,如下:

class A:      @property      def AAA(self):          print('get的時候運行我啊')      @AAA.setter      def AAA(self,value):          print('set的時候運行我啊')      @AAA.deleter      def AAA(self):          print('delete的時候運行我啊')  #只有在屬性AAA定義property後才能定義AAA.setter,AAA.deleter  f1=A()  f1.AAA        # 獲取屬性  f1.AAA='aaa'  # 設置屬性  del f1.AAA    # 刪除屬性    # 列印內如下  get的時候運行我啊  set的時候運行我啊  delete的時候運行我啊

第二種方式

class B:      def get_AAA(self):  # 獲取屬性時          print('get的時候運行我啊')      def set_AAA(self,value):  # 設置屬性          print('set的時候運行我啊')      def delete_AAA(self):   # 刪除屬性          print('delete的時候運行我啊')      AAA=property(get_AAA,set_AAA,delete_AAA) #內置property三個參數與get,set,delete一一對應    f1=B()  f1.AAA       # 獲取屬性  f1.AAA='aaa' # 設置屬性  del f1.AAA   # 刪除屬性    # 列印內容如下  get的時候運行我啊  set的時候運行我啊  delete的時候運行我啊

下面是一個商品實例的應用:

class Goods(object):      def __init__(self):          self.price_1 = 100     # 原價          self.discount_1 = 0.8  # 折扣      @property      def price(self):          new_price = self.price_1 * self.discount_1  # 實際價格 = 原價 * 折扣          return new_price      @price.setter      def price(self, value):          self.original_price = value  # 重新設置價格      @price.deleter      def price(self):  # 刪除價格          del self.original_price  obj = Goods()  obj.price         # 獲取商品價格  obj.price = 200   # 修改商品原價  del obj.price     # 刪除商品原價

isinstance和issubclass的區別

isinstance(a,b):判斷a是否是b類(或者b類的派生類)實例化的對象

如下程式碼:

class A:      pass  class B(A):      pass  obj = B()  print(isinstance(obj,B))  print(isinstance(obj,A))    # 列印內容如下  True  True

issubclass(a,b): 只能判斷a類是否是b類的派生類。

class A:      pass  class B(A):      pass  class C(B):      pass  print(issubclass(C,B))  print(isinstance(C,A))    # 列印內容如下  True  False

元類type。

按照Python的一切皆對象理論,類其實也是一個對象,那麼類這個對象是從哪裡實例化出來的呢?

class A:      pass  print(isinstance(A, type))  print(isinstance(A, object))    print(isinstance(object,type)) # object是type的實例化對象  print(issubclass(type, object))# 而type又是object的子類    # 列印內如下  True  True  True  True

type元類是獲取該對象從屬於的類,而type類比較特殊,Python原則是:一切皆對象,其實類也可以理解為'對象',而type元類又稱作構建類,python中大多數內置的類(包括object)以及自己定義的類,都是由type元類創造的。

* 而type類與object類之間的關係比較獨特:object是type類的實例,而type類是object類的子類,這種關係比較神奇無法使用python的程式碼表述,因為定義其中一個之前另一個必須存在。所以這個只作為了解。有時間在研究。