1. 程式人生 > 實用技巧 >Python基礎學習筆記(24)利用類理解queue和stack 經典類與新式類 抽象類 多型 鴨子型別

Python基礎學習筆記(24)利用類理解queue和stack 經典類與新式類 抽象類 多型 鴨子型別

Python基礎學習(24)利用類理解queue和stack 經典類與新式類 抽象類 多型 鴨子型別

一、今日內容大綱

  • 利用類理解 queue 和 stack(類的練習)
  • 自定義 Pickle 類(類的練習)
  • 經典類、新式類和 C3 演算法
  • 抽象類
  • 多型
  • 鴨子型別 Duck Typing

二、利用類理解 queue 和 stack

在引入佇列(queue)和棧(stack)的概念的同時,我們需要引入一個數據結構(Data Structure)的概念,佇列和棧都屬於資料結構的一種。

  • 資料結構:相互之間存在一種或多種特定關係的資料元素的集合。
  • 佇列:是一種特殊的線性表,它滿足FIFO(First In First Out)的條件,只能從一端進入且只能從另一端取出。
  • 棧:是一種特殊的線性表,它滿足 LIFO(Last In First Out)的條件,只能從一端進入且只能從此端取出。

現在我們利用類的方式,實現自定義類的棧和佇列(偽資料結構,只實現功能):

class Lis:
    def __init__(self):
        self.elements = []
        self.length = 0

    def my_push(self, *args):
        for i in args:
            self.elements.append(i)
            self.length += 1

    def my_pop(self):
        ret = self.elements.pop()
        self.length -= 1
        return ret


class Queue(Lis):
    def __init__(self):
        Lis.__init__(self)
        self.tp = 'queue'

    def my_pop(self):
        return self.elements.pop(0)


class Stack(Lis):
    def __init__(self):
        Lis.__init__(self)
        self.tp = 'stack'

    def my_pop(self):
        ret = Lis.my_pop(self)
        return ret


q = Queue()
s = Stack()
q.my_push(5, 2, 3, 4, 5, 6)
for i in range(q.length):
    print(q.my_pop())
s.my_push(5, 2, 3, 4, 5, 6)
for i in range(s.length):
    print(s.my_pop())

三、自定義 Pickle 類

自定義 Pickle 類,藉助 pickle 模組來完成簡化的 dump 和 load:

class My_pickle:
    def __init__(self, path):
        self.path = path
        self.load_count = 1

    def my_load(self):
        with open(self.path, mode='rb') as file_handler:
            for i in range(self.load_count):
                ret = pickle.load(file_handler)
        self.load_count += 1
        return ret

    def my_dump(self, obj):
        with open(self.path, mode='ab') as file_handler:
            pickle.dump(obj, file_handler)

# 我們藉助剛剛實現的
s = Stack()
q = Queue()
s.my_push(5, 2, 3, 4, 5, 6)
q.my_push(5, 2, 3, 4, 5, 6)
print(s, q)
obj = My_pickle('temp')
obj.my_dump(s)
obj.my_dump(q)
obj_out1 = obj.my_load()
obj_out2 = obj.my_load()
print(obj_out1)
print(obj_out2)

四、經典類、新式類和 C3 演算法

今天主要介紹經典類、新式類的繼承演算法,首先先介紹什麼是經典類和新式類:

  • 經典類:在 Python3x中不存在,在Python2x中不主動繼承object的類都是經典類,一般在繼承時採取深度優先查詢DFS策略。
  • 新式類:只要繼承object的類就是新式類,Python3x中所有的類都繼承object類,Python3x中所有的類都是新式類,一般在繼承時採取廣度優先查詢 BFS 策略(實際上是使用和 BFS 略有不同的 C3 演算法)。

而繼承時如何查詢父類的方法的問題,我們稱為 MRO(method resolution order) 問題,一組類的 MRO 序列就是在呼叫方法時在父類中查詢方法的順序序列:

# 在單繼承方面
class A:
    def func(self): pass
class B(A):
    def func(self): pass
class C(B):
    def func(self): pass
class D(C):
    def func(self): pass
# 尋找順序D->C->B->A
# 深度優先

# 多繼承
class A:
    def func(self): print('A')
class B(A):
    # def func(self): print('B')
    pass
class C(A):
    def func(self): print('C')
class D(B, C):
    # def func(self): print('D')
    pass
# 新式查詢順序D->B->C->A 廣度優先
# 經典類查詢順序D->B->A->C 深度優先
temp = D()
temp.func()


# 新式類查詢使用C3演算法
#    A              A(object) = [AO]
#  /   \            B(A)      = [BAO]
# B     C           C(A)      = [CAO]
# |     |           D(B)      = [DBAO]
# D     E           E(C)      = [ECAO]
#  \   /            F(D,E)    = merge(D(B) + E(C))
#    F                        = merge([F] + [DBAO] + [ECAO])
#                             = [F] + merge([DBAO] + [ECAO])
#                             = [FD] + merge([BAO] + [ECAO])
#                             = [FDB] + merge([AO] + [ECAO])
#                             = [FDBE] + merge([AO] + [CAO])
#                             = [FDBEC] + merge([AO] + [AO])
#                             = [FDBECA] + merge([O] + [O])
#                             = [FDBECAO]

C3 演算法規則,其實就是一個不斷進行merge操作的過程,它遵循的基本流程如下:

  • 從左只有讀取到的第一個在其他臨近節點遍歷順序未出現過的節點;
  • 同時出現在每個臨近節點第一個遍歷順序的節點;
  • 提取後再節點遍歷順序中刪除已加入節點。

另外,新式類本身也提供了一種方法classname.mro()返回 MRO 順序。

五、抽象類

首先先介紹什麼是抽象類:

  • 抽象類:是一種開發的規範,約束他的子類必須擁有和它方法裡同名的方法。

那麼我麼需要再什麼樣的情境下使用抽象類呢,加入我們在某購物網站編寫了微信支付和支付寶的支付程式:

# 支付程式:
# 微信支付:url連結,告訴你引數是什麼格式
# {'username': 'alex', 'money': 200}
# 支付寶支付: url連結,也告訴你引數是什麼格式
# {'uname': 'alex', 'price': 200}

class Alipay:
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'uname': self.name, 'price': money}
        # 想辦法呼叫支付寶支付 url連結 把dic傳過去
        print('%s通過支付寶支付%s成功' % (self.name, money))


class WechatPay:
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'username': 'alex', 'money': 200}
        # 想辦法呼叫微信支付 url連結 把dic傳過去
        print('%s通過微信支付%s成功' % (self.name, money))


aw = WechatPay('alex')
aw.pay(400)  # alex通過微信支付400成功
aa = Alipay('alex')
aa.pay(500)  # alex通過支付寶支付500成功

在實現基本功能後,我們對其進行了歸一化設計:

# 歸一化設計
def pay(name, price, mode):
    if mode == 'Wechat':
        obj = WechatPay(name)
    elif mode == 'Alipay':
        obj = Alipay(name)
    obj.pay(price)


pay('alex', 400, 'Wechat')  # alex通過微信支付400成功
pay('alex', 500, 'Alipay')  # alex通過支付寶支付500成功

這時又來了一個程式設計師,它來負責寫蘋果支付的支付程式並植入歸一化設計中,而他是這麼寫的:

class Applepay:
    def __init__(self, name):
        self.name = name

    def fuqian(self, money):
        dic = {'name': self.name, 'qian': money}
        # 想辦法呼叫蘋果支付 url連結 把dic傳過去
        print('%s通過蘋果支付%s成功' % (self.name, money))
        

# 我們按照原來的規則歸一化設計
def pay(name, price, mode):
    if mode == 'Wechat':
        obj = WechatPay(name)
    elif mode == 'Alipay':
        obj = Alipay(name)
    elif mode == 'Applepay':
        obj = Applepay(name)
    obj.pay(price)
    
    
# 但是由於蘋果支付類的設計裡用的方法名不是pay,發現使用過程中會報錯
pay('alex', 400, 'Wechat')  # alex通過微信支付400成功
pay('alex', 500, 'Alipay')  # alex通過支付寶支付500成功
pay('alex', 500, 'Applepay')  # 報錯

這時我們就可以約定一個規範,讓他如果在類定義中不新增方法pay就會報錯:

# 我們可以約定一個規範,如果他在類定義中不新增方法pay就會出現報錯
class Payment:
    def pay(self, money):
        raise NotImplementedError('請在子類中重寫同名pay方法')


# 然後將其他的支付函式繼承這個Payment類
class Alipay(Payment):
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'uname': self.name, 'price': money}
        # 想辦法呼叫支付寶支付 url連結 把dic傳過去
        print('%s通過支付寶支付%s成功' % (self.name, money))


class WechatPay(Payment):
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'username': 'alex', 'money': 200}
        # 想辦法呼叫微信支付 url連結 把dic傳過去
        print('%s通過微信支付%s成功' % (self.name, money))


class Applepay(Payment):
    def __init__(self, name):
        self.name = name

    def fuqian(self, money):
        dic = {'name': self.name, 'qian': money}
        # 想辦法呼叫蘋果支付 url連結 把dic傳過去
        print('%s通過蘋果支付%s成功' % (self.name, money))


def pay(name, price, mode):
    if mode == 'Wechat':
        obj = WechatPay(name)
    elif mode == 'Alipay':
        obj = Alipay(name)
    elif mode == 'Applepay':
        obj = Applepay(name)
    obj.pay(price)


# pay('alex', 400, 'Wechat')  # alex通過微信支付400成功
# pay('alex', 500, 'Alipay')  # alex通過支付寶支付500成功
# pay('alex', 500, 'Applepay')  # NotImplementedError: 請在子類中重寫同名pay方法

那麼,為什麼定義一個共同父類會實現這樣的功能呢?它的實際流程實際是,在我們自己定義的類中尋找pay方法,沒有找到回去Payment類中尋找,這時會根據Payment類的方法pay丟擲異常。

另外也可以利用abc模組中的ABCMetaabstractmethod實現抽象類,具體編寫方法如下:

# 另一種方式
from abc import  ABCMeta, abstractmethod
class Payment(metaclass=ABCMeta):
    @abstractmethod
    def pay(self, money): pass

class Alipay(Payment):
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'uname': self.name, 'price': money}
        # 想辦法呼叫支付寶支付 url連結 把dic傳過去
        print('%s通過支付寶支付%s成功' % (self.name, money))


class WechatPay(Payment):
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'username': 'alex', 'money': 200}
        # 想辦法呼叫微信支付 url連結 把dic傳過去
        print('%s通過微信支付%s成功' % (self.name, money))


class Applepay(Payment):
    def __init__(self, name):
        self.name = name

    def fuqian(self, money):
        dic = {'name': self.name, 'qian': money}
        # 想辦法呼叫蘋果支付 url連結 把dic傳過去
        print('%s通過蘋果支付%s成功' % (self.name, money))

# 用這種方式約束力很強,連例項化都不行,但是依賴abc模組
Applepay('alex')  # TypeError: Can't instantiate abstract class Applepay with abstract methods pay

六、多型

我們知道,在 Python 中處處皆多型,一切皆物件;現在我們已經瞭解了什麼是物件,那麼什麼是多型呢,又為什麼處處皆多型呢?

我們先引入一段 java 程式碼:

def add(int a, int b):
	return a + b

我們可以看到,與 Python 不同,Java 的形式引數在定義時,都會規定資料型別,而 Python 這種無需規定資料型別的呼叫其實就是多型,因為 Python 語法不用宣告變數型別,所以 Python 一切皆多型:

  • 多型:一個型別表現出來的多種狀態,在 Java 中可以將多個類指定一個共同的父類來實現。

我們引入剛剛編寫的支付程式程式碼:

# 引用之前的歸一化設計程式碼
class Alipay:
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'uname': self.name, 'price': money}
        # 想辦法呼叫支付寶支付 url連結 把dic傳過去
        print('%s通過支付寶支付%s成功' % (self.name, money))


class WechatPay:
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'username': 'alex', 'money': 200}
        # 想辦法呼叫微信支付 url連結 把dic傳過去
        print('%s通過微信支付%s成功' % (self.name, money))


# 歸一化設計
# def pay(WechatPay obj, int price):
#     obj.pay(price)
# 在這中情況下出現問題,因為微信支付和阿里支付的型別不同,而只能通過下面這種笨拙的方法支付:
    
obj1 = WechatPay('alex')
pay(obj1, 400)

obj2 = Alipay('alex')
pay(obj2, 500)

這時我們引入一個共同的父類:

# 這種情況下我們可以利用這種方式實現多型,定義一個新的父類Payment,讓兩個支付方式的類都繼承它
class Payment: pass
class Alipay(Payment):
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'uname': self.name, 'price': money}
        # 想辦法呼叫支付寶支付 url連結 把dic傳過去
        print('%s通過支付寶支付%s成功' % (self.name, money))


class WechatPay(Payment):
    def __init__(self, name):
        self.name = name

    def pay(self, money):
        dic = {'username': 'alex', 'money': 200}
        # 想辦法呼叫微信支付 url連結 把dic傳過去
        print('%s通過微信支付%s成功' % (self.name, money))

# 這樣我們就可以把歸一化的函式修改成這樣:
def pay(Payment obj, int price):
    obj.pay(price)

七、鴨子型別

鴨子型別和多型的概念息息相關," When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.",就是指一個鳥走路像鴨子游泳也像鴨子叫聲也像鴨子,那麼這隻鳥就可以叫做鴨子。在 Python 中,鴨子型別就是指只關注物件能夠實現的方法,而不關注物件本身的型別,如 Python 中的迭代器:它只關注物件本身能否具有__iter__方法和__next__方法,而不關注物件本身的型別。