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
模組中的ABCMeta
和abstractmethod
實現抽象類,具體編寫方法如下:
# 另一種方式
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__
方法,而不關注物件本身的型別。