Python中的接口、抽象基類(lèi)和協(xié)議示例詳解
接口與抽象基類(lèi)
在面向?qū)ο蟮氖澜缰?,我們?yīng)該對(duì)接口編程,而不是實(shí)現(xiàn)。我們的代碼應(yīng)該依賴(lài)于對(duì)象能做什么(抽象),而不是對(duì)象具體是誰(shuí)(具體實(shí)現(xiàn))。
下面通過(guò)吃漢堡的例子來(lái)演示一下。
依賴(lài)具體實(shí)現(xiàn)
class BigMac:
def eat(self):
return "巨無(wú)霸:雙層牛肉,醬汁濃郁!"
class ChickenBurger:
def eat(self):
return "香辣雞腿堡:外酥里嫩!"
class Human:
def eat_lunch(self, food: BigMac):
# 【面向?qū)崿F(xiàn)編程】
# 這里寫(xiě)死了:我只檢查是不是巨無(wú)霸。
# 如果想吃香辣雞腿堡,必須修改 Human 類(lèi)的代碼!沒(méi)有任何拓展性
if isinstance(food, BigMac):
print(f"人類(lèi)正在吃: {food.eat()}")
else:
print("人類(lèi)拒絕進(jìn)食:這不是巨無(wú)霸!")
# --- 代碼運(yùn)行 ---
person = Human()
mcd = BigMac()
kfc = ChickenBurger()
# 一旦換了不同的漢堡(具體實(shí)現(xiàn))就吃不了了。。。
person.eat_lunch(mcd)
person.eat_lunch(kfc)通過(guò)上面的例子可以看到,一旦要換不同具體實(shí)現(xiàn)的漢堡,就必須修改人類(lèi)的代碼。
面向接口編程實(shí)現(xiàn)依賴(lài)倒置
from abc import ABC, abstractmethod
# 1. 定義接口,這是漢堡基類(lèi),可以實(shí)現(xiàn)出不同的漢堡
class Burger(ABC):
@abstractmethod
def eat(self):
pass
# 2. 具體實(shí)現(xiàn)(必須繼承 Burger)
class BigMac(Burger):
def eat(self):
return "巨無(wú)霸:雙層牛肉!"
class ChickenBurger(Burger):
def eat(self):
return "香辣雞腿堡:脆皮炸雞!"
# 3. 客戶(hù)端
class Human:
# 【面向接口編程】后續(xù)想挑戰(zhàn)什么漢堡都可以,繼承Burger實(shí)現(xiàn)eat就可以
# 只要是 Burger 的子類(lèi),我都能接受
def eat_lunch(self, food: Burger):
print(f"人類(lèi)正在吃: {food.eat()}")
# --- 代碼執(zhí)行 ---
person = Human()
person.eat_lunch(BigMac())
person.eat_lunch(ChickenBurger())通過(guò)上面的代碼可以看到,我們使用了abc寫(xiě)了一個(gè)抽象基類(lèi)Burger,對(duì)于類(lèi)Human的eat_lunch方法只是依賴(lài)了Burger,而不是依賴(lài)了具體某個(gè)諸如ChickenBurger的實(shí)現(xiàn)類(lèi)。后續(xù)Human想要吃別的漢堡,我們一行代碼都不需要改,只需要繼承基類(lèi),然后實(shí)現(xiàn)具體的類(lèi)就可以了,這就是依賴(lài)倒置!
而這種使用抽象基類(lèi)實(shí)現(xiàn)依賴(lài)倒置的方式,是一種硬契約!
Python中的類(lèi)型檢查
請(qǐng)注意,在Python中的類(lèi)型注解并不會(huì)強(qiáng)制檢查,而只是一種靜態(tài)檢查,Python解釋器不會(huì)去理會(huì)參數(shù)的類(lèi)型注解。
from abc import ABC, abstractmethod
# 1. 定義接口,這是漢堡基類(lèi),可以實(shí)現(xiàn)出不同的漢堡
class Burger(ABC):
@abstractmethod
def eat(self):
pass
class Dumpling(object):
def eat(self):
return "玉米餃子"
# 3. 客戶(hù)端
class Human:
# 【面向接口編程】后續(xù)想挑戰(zhàn)什么漢堡都可以,繼承Burger實(shí)現(xiàn)eat就可以
# 只要是 Burger 的子類(lèi),我都能接受
def eat_lunch(self, food: Burger):
print(f"人類(lèi)正在吃: {food.eat()}")
# --- 代碼執(zhí)行 ---
person = Human()
# 傳入的對(duì)象是餃子的實(shí)例對(duì)象,但是也可以運(yùn)行成功,因?yàn)轱溩佑衑at方法
person.eat_lunch(Dumpling())可以看到,即使上面?zhèn)魅氲牟皇秋@式繼承漢堡基類(lèi)的實(shí)例,也可以成功運(yùn)行。因?yàn)轭?lèi)型注解只是靜態(tài)檢查。如果我們想要運(yùn)行時(shí)檢查,就得使用isinstance()顯式檢查。
from abc import ABC, abstractmethod
# 1. 定義接口,這是漢堡基類(lèi),可以實(shí)現(xiàn)出不同的漢堡
class Burger(ABC):
@abstractmethod
def eat(self):
pass
class Dumpling(object):
def eat(self):
return "玉米餃子"
# 3. 客戶(hù)端
class Human:
# 【面向接口編程】后續(xù)想挑戰(zhàn)什么漢堡都可以,繼承Burger實(shí)現(xiàn)eat就可以
# 只要是 Burger 的子類(lèi),我都能接受
def eat_lunch(self, food: Burger):
if not isinstance(food, Burger):
raise TypeError(f"必須傳入 Burger 的子類(lèi),當(dāng)前傳入的是: {type(food).__name__}")
print(f"人類(lèi)正在吃: {food.eat()}")
# --- 代碼執(zhí)行 ---
person = Human()
person.eat_lunch(Dumpling())虛擬子類(lèi)
但是這樣做,其實(shí)也可以通過(guò)騷操作繞過(guò)去——虛擬子類(lèi)
from abc import ABC, abstractmethod
# 1. 定義接口,這是漢堡基類(lèi),可以實(shí)現(xiàn)出不同的漢堡
class Burger(ABC):
@abstractmethod
def eat(self):
pass
# 注冊(cè)為漢堡的虛擬子類(lèi)
@Burger.register
class Dumpling(object):
def eat(self):
return "玉米餃子"
# 3. 客戶(hù)端
class Human:
# 【面向接口編程】后續(xù)想挑戰(zhàn)什么漢堡都可以,繼承Burger實(shí)現(xiàn)eat就可以
# 只要是 Burger 的子類(lèi),我都能接受
def eat_lunch(self, food: Burger):
if not isinstance(food, Burger):
raise TypeError(f"必須傳入 Burger 的子類(lèi),當(dāng)前傳入的是: {type(food).__name__}")
print(f"人類(lèi)正在吃: {food.eat()}")
# --- 代碼執(zhí)行 ---
person = Human()
person.eat_lunch(Dumpling())上面代碼通過(guò)把餃子注冊(cè)為漢堡的虛擬子類(lèi),代碼可以跑通。
但是,從純粹的面向?qū)ο罄碚搧?lái)看,頻繁使用普通類(lèi)去進(jìn)行 isinstance() 檢查,確實(shí)在很大程度上違背了 Python 核心的“鴨子類(lèi)型”(Duck Typing)設(shè)計(jì)哲學(xué)。在Python中其實(shí)并不怎么關(guān)注血統(tǒng),更在意的其實(shí)是能力。
協(xié)議
在計(jì)算機(jī)世界里面,在不同語(yǔ)境下的協(xié)議具有不同的意思。比如我們最熟悉的HTTP這種網(wǎng)絡(luò)協(xié)議指明了客戶(hù)端可向服務(wù)器發(fā)送的命令,例如get,put,post。而Python中的對(duì)象協(xié)議則指明了為履行某個(gè)角色,對(duì)象必須實(shí)現(xiàn)哪些方法。協(xié)議相對(duì)于上面展示的面向接口編程,其實(shí)是更高層級(jí)的抽象,完美詮釋了鴨子類(lèi)型的設(shè)計(jì)哲學(xué),只關(guān)注對(duì)象擁有什么能力,而不是去關(guān)注血統(tǒng)(父子類(lèi)顯式繼承)。
一個(gè)展示Python協(xié)議神奇之處的例子:
from collections.abc import Iterable
class MyIter:
# MyIter類(lèi)只是定義了__iter__方法
def __iter__(self):
pass
counter = MyIter()
# 神奇的事情發(fā)生了,MyIter和Iterable并沒(méi)有任何顯式繼承的關(guān)系
print(isinstance(counter, Iterable))
# 驗(yàn)證一下MyIter并沒(méi)有繼承Iterable
print(MyIter.__mro__)如果不熟悉Python的讀者一定會(huì)對(duì)代碼的結(jié)果感到驚訝,明明MyIter和Iterable并沒(méi)有任何顯式繼承的關(guān)系,但是isinstance(counter, Iterable)居然返回了Ture!這就是Python協(xié)議的強(qiáng)大之處,目標(biāo)有__iter__(迭代)的能力,我就認(rèn)為你是一個(gè)迭代器,無(wú)需任何顯式繼承。
上下文管理器協(xié)議(Context Manager Protocol)
Python 的協(xié)議(Protocol)打破了接口的最后一道枷鎖——顯式繼承。它告訴我們,最高級(jí)的抽象不是去畫(huà)一張完美無(wú)缺的物種分類(lèi)圖,而是去定義一套純粹的行為契約。
# 1. 一個(gè)正經(jīng)的后端對(duì)象:數(shù)據(jù)庫(kù)連接
class DatabaseConnection:
def __enter__(self):
print("?? [Database] 開(kāi)啟數(shù)據(jù)庫(kù)連接...")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("?? [Database] 釋放數(shù)據(jù)庫(kù)連接...")
return False
# 2. 一個(gè)毫不相干的奇葩對(duì)象:魔法傳送門(mén)
class MagicalPortal:
def __enter__(self):
print("?? [Portal] 念動(dòng)咒語(yǔ),打開(kāi)時(shí)空傳送門(mén)...")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("?? [Portal] 傳送結(jié)束,關(guān)閉傳送門(mén)防止怪物入侵...")
return False
# 3. 見(jiàn)證協(xié)議的威力
print("--- 場(chǎng)景 A:后端工程師在工作 ---")
# with 語(yǔ)句根本不在乎你是數(shù)據(jù)庫(kù)還是什么,它只看協(xié)議
with DatabaseConnection():
print(" 執(zhí)行 SQL 查詢(xún): SELECT * FROM users")
print("\n--- 場(chǎng)景 B:法師在施法 ---")
# 魔法傳送門(mén)沒(méi)有繼承任何基類(lèi),但它滿(mǎn)足了協(xié)議,照樣能用 with!
with MagicalPortal():
print(" 正在穿越到艾澤拉斯大陸...")通過(guò)上面的例子(ai寫(xiě)的)可以看出,兩個(gè)上下文管理對(duì)象沒(méi)有任何顯式繼承,但是卻能被with這個(gè)上下文管理器。原因就在于DatabaseConnection和MagicalPortal都實(shí)現(xiàn)了上下文管理器的協(xié)議。
靜態(tài)協(xié)議
Protocol(靜態(tài)協(xié)議)是Python在3.8引入的,通過(guò)上面的關(guān)于協(xié)議的講解,我們可以發(fā)現(xiàn),協(xié)議的壞處就是沒(méi)有類(lèi)型提示,所以往往我們可能去調(diào)用一個(gè)對(duì)象不存在的方法,而我們很難在寫(xiě)代碼的時(shí)候通過(guò)Mypy或者是IDE發(fā)現(xiàn)。
from typing import Protocol
# 1. 定義靜態(tài)協(xié)議 (契約)
# 注意:我們繼承的是 typing.Protocol,而不是常規(guī)的基類(lèi)
class MessageSender(Protocol):
def send(self, message: str) -> bool:
"""只要一個(gè)類(lèi)擁有接受 str 并返回 bool 的 send 方法,
它在靜態(tài)檢查器眼里,就是 MessageSender。"""
# 2. 具體實(shí)現(xiàn) (完全不需要繼承 MessageSender!)
class EmailSender:
def send(self, message: str) -> bool:
print(f"?? 發(fā)送郵件: {message}")
return True
class SMSSender:
def send(self, message: str) -> bool:
print(f"?? 發(fā)送短信: {message}")
return True
# 3. 一個(gè)“假裝”能發(fā)送,但其實(shí)方法名不對(duì)的類(lèi)
class Printer:
def print_doc(self, message: str) -> bool:
print(f"??? 打印文檔: {message}")
return True
# 4. 客戶(hù)端調(diào)用方
# 在類(lèi)型注解中指定依賴(lài) Protocol
def alert_admin(sender: MessageSender, alert_msg: str):
sender.send(alert_msg)
# --- 測(cè)試環(huán)節(jié) ---
email_sender = EmailSender()
sms_sender = SMSSender()
printer = Printer()
# ? 這兩行在 IDE(如 VS Code / PyCharm)或 mypy 中完全合法
alert_admin(email_sender, "服務(wù)器 CPU 超過(guò) 90%!")
alert_admin(sms_sender, "數(shù)據(jù)庫(kù)連接斷開(kāi)!")
# ? 這一行會(huì)在代碼運(yùn)行前,直接被 IDE 標(biāo)紅報(bào)錯(cuò)!
alert_admin(printer, "機(jī)房起火了!")@runtime_checkable
在使用 typing.Protocol 時(shí),我們獲得了極佳的靜態(tài)類(lèi)型提示體驗(yàn)(IDE 提供智能補(bǔ)全,mypy 靜態(tài)檢查能通過(guò))。但是,由于 Protocol 純粹是為“靜態(tài)檢查”設(shè)計(jì)的,如果你嘗試在代碼運(yùn)行階段使用 isinstance() 去驗(yàn)證一個(gè)對(duì)象是否符合靜態(tài)協(xié)議,Python 會(huì)直接拒絕執(zhí)行并拋出異常!
from typing import Protocol
class Burger(Protocol):
def eat(self) -> str:
...
class BigMac:
def eat(self) -> str:
return "巨無(wú)霸:雙層牛肉!"
# 運(yùn)行這段代碼會(huì)直接報(bào)錯(cuò):
print(isinstance(BigMac(), Burger))為了解決這個(gè)問(wèn)題,讓協(xié)議既能享受靜態(tài)類(lèi)型檢查的紅利,又能像普通的類(lèi)一樣在運(yùn)行時(shí)使用 isinstance() 進(jìn)行鴨子類(lèi)型判斷,Python 的 typing 模塊提供了 @runtime_checkable 裝飾器。
它的底層原理,正是我們?cè)谏衔奶岬降?__subclasshook__ 黑魔法。加上這個(gè)裝飾器后,Python 會(huì)自動(dòng)在底層攔截 isinstance() 調(diào)用,去動(dòng)態(tài)檢查對(duì)象是否擁有協(xié)議中定義的那些方法或?qū)傩浴?/p>
from typing import Protocol, runtime_checkable
# 加上裝飾器,賦予協(xié)議在運(yùn)行時(shí)被 isinstance 檢查的能力
@runtime_checkable
class Burger(Protocol):
def eat(self) -> str:
...
class BigMac:
def eat(self) -> str:
return "巨無(wú)霸:雙層牛肉!"
class Hotdog:
def lick(self) -> str:
return "吃熱狗"
mac = BigMac()
dog = Hotdog()
# ? 運(yùn)行時(shí)完美通過(guò)!輸出 True,因?yàn)?BigMac 擁有 eat 方法
print(f"mac 是 Burger 嗎? {isinstance(mac, Burger)}")
# ? 完美攔截!輸出 False,因?yàn)?Hotdog 沒(méi)有 eat 方法
print(f"dog 是 Burger 嗎? {isinstance(dog, Burger)}")使用協(xié)議實(shí)現(xiàn)依賴(lài)倒置
上面我們?cè)谥v接口和抽象基類(lèi)的時(shí)候,通過(guò)一個(gè)漢堡的例子向讀者展示怎樣實(shí)現(xiàn)依賴(lài)倒置。這是通過(guò)定義抽象基類(lèi),讓具體實(shí)現(xiàn)繼承抽象基類(lèi)Burger這個(gè)“硬契約”來(lái)實(shí)現(xiàn)的。而Python中的協(xié)議其實(shí)是更加深層次的抽象,我們無(wú)需顯式繼承,通過(guò)實(shí)現(xiàn)一套“軟契約”來(lái)實(shí)現(xiàn)。如果說(shuō)繼承是強(qiáng)硬法律的話(huà),協(xié)議則更像是一種君子協(xié)定。
from typing import Protocol
# 1. 定義協(xié)議(靜態(tài)接口/結(jié)構(gòu)化子類(lèi)型)
class Burger(Protocol):
def eat(self) -> str:
...
# 2. 具體實(shí)現(xiàn)(隱式實(shí)現(xiàn)了 Burger 協(xié)議,完全解耦,無(wú)需顯式繼承)
class BigMac:
def eat(self) -> str:
return "巨無(wú)霸:雙層牛肉!"
class ChickenBurger:
def eat(self) -> str:
return "香辣雞腿堡:脆皮炸雞!"
# 3. 客戶(hù)端
class Human:
# 【面向協(xié)議編程/靜態(tài)鴨子類(lèi)型】
# 只要傳入的對(duì)象擁有和 Burger 協(xié)議定義一致的 eat 方法,就能通過(guò)靜態(tài)類(lèi)型檢查
def eat_lunch(self, food: Burger) -> None:
print(f"人類(lèi)正在吃: {food.eat()}")
# --- 代碼執(zhí)行 ---
person = Human()
person.eat_lunch(BigMac())
person.eat_lunch(ChickenBurger())通過(guò)上面的代碼,我們可以看到,我們實(shí)現(xiàn)的這套“軟契約”其實(shí)就是目標(biāo)對(duì)象有沒(méi)有eat這個(gè)方法,有我就認(rèn)為你是漢堡。為什么說(shuō)這是更加抽象的方式,因?yàn)槲覀冊(cè)诮怦盍藵h堡的具體實(shí)現(xiàn)與人類(lèi)的耦合的同時(shí),還無(wú)需去顯式繼承任何基類(lèi)。
顯式繼承 (ABC):不僅要求類(lèi)具備某些方法,還強(qiáng)制要求類(lèi)在“族譜”上屬于某個(gè)基類(lèi)(強(qiáng)耦合的
is-a關(guān)系)。協(xié)議 (Protocol):完全不在乎對(duì)象是從哪里來(lái)的、繼承自誰(shuí),只關(guān)心它能不能做這件事(
can-do關(guān)系)。實(shí)現(xiàn)類(lèi)根本不需要知道協(xié)議的存在,也不需要導(dǎo)入?yún)f(xié)議所在的模塊,這就實(shí)現(xiàn)了真正的業(yè)務(wù)邏輯與接口定義的徹底解耦。
協(xié)議黑魔法
現(xiàn)在回到我們開(kāi)頭那個(gè)迭代器協(xié)議的例子,Python是如何“偷偷”實(shí)現(xiàn)這個(gè)協(xié)議的
其實(shí)秘密就在于__subclasshook__這個(gè)魔法方法,
當(dāng)我們調(diào)用 isinstance(obj, SomeABC) 時(shí),Python 內(nèi)部的邏輯流如下:
顯式繼承,檢查
obj的類(lèi)是否繼承自SomeABC。虛擬子類(lèi)注冊(cè),檢查
obj的類(lèi)是否通過(guò)SomeABC.register()注冊(cè)過(guò)。關(guān)鍵點(diǎn):調(diào)用
SomeABC.__subclasshook__(cls)。如果這個(gè)方法返回True,那么即使前兩條都不滿(mǎn)足,isinstance也會(huì)返回True。
所以Iterable協(xié)議就是通過(guò)定義__subclasshook__這個(gè)方法,這個(gè)方法會(huì)去檢查_(kāi)_dict__里面會(huì)不會(huì)存在__iter__這個(gè)鍵,如果存在就返回True。
本文向讀者講解了如何面向接口編程,不去依賴(lài)具體的實(shí)現(xiàn),實(shí)現(xiàn)依賴(lài)倒置。講解了兩種方式,強(qiáng)契約:基類(lèi)繼承,軟契約:協(xié)議。
總結(jié)
到此這篇關(guān)于Python中的接口、抽象基類(lèi)和協(xié)議的文章就介紹到這了,更多相關(guān)Python接口、抽象基類(lèi)和協(xié)議內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
pycharm 中mark directory as exclude的用法詳解
今天小編就為大家分享一篇pycharm 中mark directory as exclude的用法詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-02-02
python輸出第n個(gè)默尼森數(shù)的實(shí)現(xiàn)示例
這篇文章主要介紹了python輸出第n個(gè)默尼森數(shù)的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
最新Python爬蟲(chóng)從入門(mén)到精通:靜態(tài)爬取+JS逆向+反爬繞過(guò)實(shí)戰(zhàn)教程
本文從零基礎(chǔ)出發(fā),系統(tǒng)講解Python爬蟲(chóng)的靜態(tài)抓取、動(dòng)態(tài)接口分析、JS逆向破解加密參數(shù)、反爬繞過(guò)(IP代理、Cookie維持、延時(shí)控制)及數(shù)據(jù)存儲(chǔ),包含完整可運(yùn)行代碼、抓包技巧、簽名算法還原、常見(jiàn)報(bào)錯(cuò)解決方案,幫助讀者真正實(shí)現(xiàn)從入門(mén)到精通2026-04-04
Python實(shí)現(xiàn)快速將Word文檔轉(zhuǎn)換為PDF的完整教程
在文檔管理和分發(fā)場(chǎng)景中,將 Word 文檔轉(zhuǎn)換為 PDF 是一項(xiàng)基礎(chǔ)且關(guān)鍵的操作,本文將深入探討如何使用 Python 將 Word 文檔高效地轉(zhuǎn)換為 PDF 格式,并控制轉(zhuǎn)換過(guò)程中的各項(xiàng)參數(shù),希望對(duì)大家有所幫助2026-03-03

