1. 程式人生 > 實用技巧 >python核心程式設計-第四章-深入類和物件

python核心程式設計-第四章-深入類和物件

第四章、深入類和物件

4.1鴨子型別

當看到一隻鳥走起來像鴨子、游泳像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子

4.2抽象基類(abc模組)

判斷某一個物件是否有某一個屬性,hasattr()

class Company(object):
    def __init__(self, employee_list):
        self.employee = employee_list

    def __getitem__(self, item):
        return self.employee[item]

    def __len__(self):
        return len(self.employee)


print(hasattr(Company, "__len__"))
company = Company(['tom', 'bob', 'jane'])
print(hasattr(company, "__len__"))

判斷某一個物件是否是某種型別,isinstance()

對於判斷物件是否可以呼叫某函式,更好的方法是去判斷其是否是某種型別,而不是該物件是否有某個屬性。

# 在某些情況下希望判定某個物件的型別,我們需要強制某個子類必須實現某些方法

from collections.abc import Sized
class Company(object):
    def __init__(self, employee_list):
        self.employee = employee_list

    def __getitem__(self, item):
        return self.employee[item]

    def __len__(self):
        return len(self.employee)


company = Company(['tom', 'bob', 'jane'])
print(isinstance(company, Sized))
# 設計一個抽象基類,指定子類必須事項某些方法
# 實現一個web框架,整合cache(redis, cache, memorychache)
class CacheBase():
    def get(self, key):
        raise NotImplementedError
    def set(self, key, value):
        raise NotImplemnetedError
   

class RedisCache(CacheBase):
    pass


redis_cache = RedisCache()
redis_cache.set("key", "value")

報錯如下示:

# 作如下更改
class RedisCache(CacheBase):

    def __init__(self):
        self.dic = {}

    def get(self, key):
        return self.dic[key] if key in self.dic else None

    def set(self, key, value):
        self.dic[key] = value


redis_cache = RedisCache()
redis_cache.set("key", "value")
print(redis_cache.get("key"))

4.3使用instance而不是type

  • type判斷一個物件是誰的例項
  • isinstance是判斷這個物件的繼承鏈,isinstance(a,b)如果a在b的下方就返回True,否則False
class A(object):
    pass


class B(A):
    pass


class C(B):
    pass


c = C()
print(type(c))  # <class '__main__.C'>
print(type(C))  # <class 'type'>
print(isinstance(C, B))  # False
print(isinstance(C, object))  # True

4.4類屬性和例項屬性以及查詢順序

先例項屬性再類屬性

如果把例項看成是類,那麼類變數就是父類中的屬性,而例項變數是子類的屬性,子類中沒有父類有 會從父類中繼承,子類中有父類有 相當於覆蓋重寫

class A:
    name = 'bb'
    def __init__(self, x):
        self.x = x


a1 = A(1)
a2 = A(2)

print(a1.name)  #繼承類變數bb
print(a1.x)  # 就是例項變數x的值
print(A.name)  # 類變數bb

A.name = 'dd'  # 更改了類變數
print(A.name)  # 類變數發生更改dd
print(a1.name)  # 例項的類變數也更改dd

a1.name = 'name'
print(a1.name)  # 在此更改類變數
print(A.name)  # 刪除了例項屬性

A.name = 'last'  # 在此更改了類變數
del a1.name  # 刪除了例項變數
print(a1.name)  # 繼承類變數last

類變數和屬性的呼叫順序:

在python中,為了重用程式碼,可以使用在子類中使用父類的方法,但是在多繼承中可能會出現重複呼叫的問題,支援多繼承的面向物件程式設計都可能會導致鑽石繼承(菱形繼承)問題 ,如下示:

class A():
    def __init__(self):
        print("進入A…")
        print("離開A…")

class B(A):
    def __init__(self):
        print("進入B…")
        A.__init__(self)
        print("離開B…")
        
class C(A):
    def __init__(self):
        print("進入C…")
        A.__init__(self)
        print("離開C…")

class D(B, C):
    def __init__(self):
        print("進入D…")
        B.__init__(self)
        C.__init__(self)
        print("離開D…")
      
d = D()
# 實際上會先找B, 然後找A, 然後是C, 再找A
'''
進入D…
進入B…
進入A…
離開A…
離開B…
進入C…
進入A…
離開A…
離開C…
離開D…
'''

鑽石繼承(菱形繼承)會帶來什麼問題?

多重繼承容易導致鑽石繼承(菱形繼承)問題,上邊程式碼例項化 D 類後我們發現 A 前後進入了兩次。另外,假設 A 的初始化方法裡有一個計數器,那這樣 D 一例項化,A 的計數器就跑兩次(如果遭遇多個鑽石結構重疊還要更多),很明顯是不符合程式設計的初衷的(程式應該可控,而不能受到繼承關係影響)。

如何避免鑽石繼承(菱形繼承)問題?

為解決這個問題,Python 使用了一個叫“方法解析順序(Method Resolution Order,MRO)”的東西,還用了一個叫 C3 的演算法。

MRO 的順序基本就是:在避免同一類被呼叫多次的前提下,使用廣度優先和從左到右的原則去尋找需要的屬性和方法。有則呼叫,沒有就查詢新增到MRO中。

在繼承體系中,C3 演算法確保同一個類只會被搜尋一次。例子中,如果一個屬性或方法在 D 類中沒有被找到,Python 就會搜尋 B 類,然後搜尋 C類,如果都沒有找到,會繼續搜尋 B 的基類 A,如果還是沒有找到,則丟擲“AttributeError”異常。

可以使用 類名.mro 獲得 MRO 的順序(注:object 是所有類的基類,金字塔的頂端):

print(D.mro())
# [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

為了防止多繼承中的一些問題,使用C3演算法解決,它能夠保證呼叫鏈中所有的類僅出現一次,也就是說每個節點後面的類都不繼承它,否則將它從呼叫鏈中刪除,D.mro()檢視呼叫鏈。

super()的使用就是基於這條呼叫鏈,當使用super()時,會查詢呼叫鏈,找到當前類,呼叫下一個類,沒有則找下一個。super()預設是當前類,也可以寫類名

class A():
    def __init__(self):
        print("進入A…")
        print("離開A…")

class B(A):
    def __init__(self):
        print("進入B…")
        super().__init__()
        print("離開B…")
        
class C(A):
    def __init__(self):
        print("進入C…")
        super().__init__()
        print("離開C…")

class D(B, C):
    def __init__(self):
        print("進入D…")
        super().__init__()
        print("離開D…")
    
    
d = D()
'''
進入D…
進入B…
進入C…
進入A…
離開A…
離開C…
離開B…
離開D…
'''

4.4.1mro序列

MRO的侷限性:型別衝突時, 子類改變了基類的方法搜尋順序。而 子類不能改變基類的方法搜尋順序。在 Python 2.2 的 MRO 演算法中並不能保證這種單調性,它不會阻止程式設計師寫出上述具有二義性的繼承關係,因此很可能成為錯誤的根源。

部落格:https://mp.weixin.qq.com/s?src=11&timestamp=1606794886&ver=2739&signature=SRS5nobqUFoRtzuAsoe8pOknkOYeNxLZz90OnfG3sQtaaMcaI23ZYMSjmGLKogS997A1whxzOgqTugCvuNSlEz5akZ86QE4myAw2JboDp-DRHrsLiWKCj97Ld2lKTq&new=1

  • MRO(Method Resolution Order: 方法解析順序) 是一個有序列表L,在類被建立時就計算出來。
  • 通用的計算公式:
mro(Child(Base1, Base2)) = [Child] + merge(mro(Base1), mro(Base2), [Base1, Base2])
 (其中Child繼承自Base1, Base2)
  • 如果繼承至一個基類:class B(A)
    這時B的mro序列為 :
mro( B ) 
= mro( B(A) ) 
= [B] + merge( mro(A) + [A] )
= [B] + merge( [A] + [A] )
= [B,A]
  • 如果繼承至多個基類:class B(A1, A2, A3 …)
    這時B的mro序列
mro(B)  
= mro( B(A1, A2, A3 …) )
= [B] + merge( mro(A1), mro(A2), mro(A3) ..., [A1, A2, A3] )
= ...

PS: 計算結果為列表,列表中至少有一個元素即類自己,如上述示例[A1,A2,A3]。merge操作是C3演算法的核心。

(參考部落格:https://blog.csdn.net/u011467553/article/details/81437780)

4.4.2 表頭和表尾

  • 表頭:
    列表的第一個元素
  • 表尾:
    列表中表頭以外的元素集合(可以為空)
  • 示例
    列表:[A, B, C]
    表頭是A,表尾是B和C

4.4.3 列表之間的+操作

+操作:

list_ = ['A'] + ['B']  
print(list_)  # ['A', 'B']

4.4.4 merge操作(C3演算法)

如計算merge( [E,O], [C,E,F,O], [C] )
有三個列表 : list1    list2   list3

1 merge不為空,取出第一個列表列表list1的表頭E,進行判斷                              
各個列表的表尾分別是[O], [E,F,O],E在這些表尾的集合中,因而跳過當前當前列表
2 取出列表list2的表頭C,進行判斷
C不在各個列表的集合中,因而將C拿出到merge外,並從所有表頭刪除
merge( [E,O], [C,E,F,O], [C]) = [C] + merge( [E,O], [E,F,O] )
3 進行下一次新的merge操作 ......
  
merge( [E,O], [C,E,F,O], [C]) 
= [C] + merge( [E,O], [E,F,O] )
= [c] + [E] + merge( [O], [F, 0])
= [c] + [E] + [F] + merge( [O], [0])
= [C] + [E] + [F] + [0]

看看之前的

print(D.mro())
# [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

mro(D(B, C))
= [D] + merge(mro(B(A)), mro(C(A)), [B, C])
= [D] + merge([B] + merge(A(object), [A]), [C] + merge(A(object), [A]), [B, C])
= [D] + merge([B] + merge([A, object], [A]), [C] + merge([A, object], [A]), [B, C])
= [D] + merge([B] + [A, object], [C] + [A, object], [B, C])
= [D] + merge([B, A, object], [C, A, object], [B, C])
= [D] + [B] + merge([A, object], [C, A, object], [C])
= [D] + [B] + [C] + merge([A, object], [A, object])
= [D] + [B] + [C] + [A] + merge([object], [object])
= [D] + [B] + [C] + [A] + [object]
= [D, B, C, A, object]
= [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

4.4.5實戰測試: 案例一

# 備註:O==object, 如何計算mro(A) ?
mro(A)
= mro(A(B, C))
= [A] + merge(mro(B(D, E)), mro(C(E, F)), [B, C])
= [A] + merge([B] + merge(mro(D(O)), mro(E(O)), [D, E]), [C] + merge(mro(E(O)), mro(F(O)), [E, F]), [B, C])
= [A] + merge([B] + merge([D, O], [E, O], [D, E]), [C] + merge([E, O], [F, O], [E, F]), [B, C])
= [A] + merge([B] + [D, E, 0], [C] + [E, F, O], [B, C])
= [A] + merge([B, D, E, 0], [C, E, F, O], [B, C])
	merge([D, O], [E, O], [D, E])
	= [D] + merge([O], [E, O], [E])
	= [D] + [E] + merge([O], [O])
	= [D] + [E] + [O]
	= [D, E, 0]
	merge([E, O], [F, O], [E, F])
	= [E] + merge([O], [F, O], [F])
	= [E] + [F] + merge([O], [O])
	= [E] + [F] + [O]
	= [E, F, O]
= [A] + [B] + merge([D, E, 0], [C, E, F, O], [C])
= [A] + [B] + [D] + merge([E, 0], [C, E, F, O], [C])
= [A] + [B] + [D] + [C] + merge([E, 0], [E, F, O])
= [A] + [B] + [D] + [C] + [E] + merge([0], [F, O])
= [A] + [B] + [D] + [C] + [E] + [F]+ merge([0], [O])
= [A] + [B] + [D] + [C] + [E] + [F]+ [O]
= [A, B, D, C, E, F, O]
# 以上案例的程式碼測試
class D: pass
class E: pass
class F: pass
class B(D,E): pass
class C(E,F): pass
class A(B,C): pass

print("從A開始查詢:")
for s in A.__mro__:
	print(s)

print("從B開始查詢:")
for s in B.__mro__:
	print(s)

print("從C開始查詢:")
for s in C.__mro__:
	print(s)
'''
從A開始查詢:
<class '__main__.A'>
<class '__main__.B'>
<class '__main__.D'>
<class '__main__.C'>
<class '__main__.E'>
<class '__main__.F'>
<class 'object'>
從B開始查詢:
<class '__main__.B'>
<class '__main__.D'>
<class '__main__.E'>
<class 'object'>
從C開始查詢:
<class '__main__.C'>
<class '__main__.E'>
<class '__main__.F'>
<class 'object'>
'''

規律總結: “從一至頂,有子先出”

4.4.6實戰測試:案例二

結果參考: