国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Python學習之路30-接口:從協議到抽象基類

LucasTwilight / 3554人閱讀

摘要:本篇內容將從鴨子類型的動態協議,逐漸過渡到使接口更明確能驗證實現是否符合規定的抽象基類。抽象基類介紹完動態實現接口后,現在開始討論抽象基類,它屬于靜態顯示地實現接口。標準庫中的抽象基類從開始,標準庫提供了抽象基類。

《流暢的Python》筆記。

本篇是“面向對象慣用方法”的第四篇,主要討論接口。本篇內容將從鴨子類型的動態協議,逐漸過渡到使接口更明確、能驗證實現是否符合規定的抽象基類(Abstract Base Class, ABC)。

1. 前言

本篇討論Python中接口的實現問題,主要內容如下:

補充用鴨子協議實現部分接口的一種重要方法:猴子補丁;

說明抽象基類的常見用途,即,實現接口時作為超類使用;

說明抽象基類如何檢查具體子類是否符合接口定義,以及如何使用注冊機制聲明一個類實現了某個接口;

說明如何不通過子類化或注冊,也能讓抽象基類自動“識別”任何符合接口的類。

補充在正文之前

在Python中,“X類對象”,“X協議”和“X接口”都是一個意思。并且,除了抽象基類,類實現或繼承的公開屬性(方法或數據屬性),包括特殊方法,都可以看做接口。

關于接口,還有一個很實用的補充定義:對象公開方法的子集,讓對象在系統中扮演特定的角色。

2. 猴子補丁

猴子補丁并不是Python特有,它指動態語言中,不用修改源代碼,在運行時就能對代碼的功能進行動態的追加或變更。下面的代碼展示了猴子補丁的用法:

# 代碼2.1
# 在文件中定義
class MyList:
    def __init__(self, iterable):
        self._data = list(iterable)

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

    def __getitem__(self, index):
        return self._data[index]

# 下面的代碼在控制臺運行
>>> from random import shuffle
>>> from my_list import MyList
>>> mylist = MyList(range(10))
>>> def set_item(temp, i, item):
...     temp._data[i] = item
...    
>>> MyList.__setitem__ = set_item
>>> shuffle(mylist)
>>> deck[:]
[6, 3, 0, 1, 5, 4, 2, 7, 9, 8]

解釋

Python中,交互式控制臺中也支持猴子補丁;

要使用random.shuffle函數,對象必須實現__setitem__方法,上述代碼在運行時動態添加所需方法;

猴子補丁很強大,但打補丁的代碼與要打補丁的程序耦合十分緊密,而且往往要處理隱藏的部分(比如“受保護的”屬性)和沒有文檔的部分。

上述代碼中set_item函數的第一個參數并不是self,這是想說明,每個Python方法說到底都是普通函數,把第一個參數命名為self只是一種約定(但別隨意打破這種約定)。

這里之所以講猴子補丁,主要是為了說明協議可以是動態的:即使對象最初沒有實現某個協議,當需要時,我們也能為它動態添加。

3. 抽象基類

介紹完動態實現接口后,現在開始討論抽象基類,它屬于靜態顯示地實現接口。

3.1 基本概要說明

有時候我們需要明確區分“抽象類”(并不是指“抽象基類”)與“接口”:以自然界為例,“抽象類”一般用于同一物種同一行為,而“接口”則用于不同物種同一行為。當然,這兩個概念有交叉的部分,某些行為既可以歸到“接口“,也可以歸到”抽象類“,而最后歸到誰就見仁見智了。但這兩個概念又有很大的相似之處,它們的實質都是:讓某些對象擁有同名的方法或屬性,但具體實現不一定相同

Java更注重這兩者的特性,而Python、C++則更注重這兩者的共性。也因此,Java不支持多重繼承(當然,也是為了降低復雜性),用明確的接口類interface來區分與abstract class;而在Python和C++中,則用抽象基類充當接口。所以,在Python中,直接繼承自抽象基類,更多表明的是”要實現某種接口或協議“,而非”要新建某個具體類的子類“。

如果要測試是否繼承自抽象基類,推薦使用isinstanceissubclass方法,而不是is運算。但也不要濫用這類方法,因為這種代碼用多了說明面向對象設計得不好。

說道isinstance,還有個與之相關的概念,相當于“鴨子類型”的強化版:

白鵝類型(goose typing):只要cls是抽象基類,即cls的元素是abc.ABCMeta,就可以使用isinstance(obj, cls)

小插曲:這是書中給出的標準定義,筆者讀到這的時候一臉懵逼。“白鵝類型”是個名詞,但這定義卻是對一個過程的描述,所以“白鵝類型”到底是個啥(這到底是翻譯的鍋還是作者的鍋)?后來谷歌了一下,再自己反復推敲,得出如下總結:鴨子類型是指某個實例實現了某個方法,就可以說它屬于某個類型,不一定要繼承;而白鵝類型則是指能被判定成某抽象基類的子類的實例,即,能使isinstance(obj, cls)返回Trueobj就是白鵝類型,其中cls是抽象基類。注意,這些子類并不一定是通過繼承而來,也可能是通過注冊而來,還可能是通過實現某些方法而來。

特別提醒:對于抽象基類(還有元類)的使用,并不建議在生產代碼中自行定義新的抽象基類和元類。定義抽象基類和元類的工作一般由比較資深的Python程序員來做,適用于寫框架的程序員。而即便是資深Python程序員也不常自己定義抽象基類和元類。

3.2 標準庫中的抽象基類

從Python2.6開始,標準庫提供了抽象基類。大多數抽象基類在collections.abc模塊中定義,numbersio中也有一些。

以下是collections.abc中16個抽象基類的UML圖(關于多重繼承的內容將在以后的文章中講解):

有幾個抽象基類值得注意:

IterableContainerSized:各個集合類應該繼承這三個抽象基類,或者至少實現兼容的協議。Iterable通過__iter__方法支持迭代;Container通過__contains__方法支持in運算;Sized通過__len__方法支持len()函數;

SequenceMappingSet:這三個是主要的不可變集合類型,而且各自都有可變的子類,即MutableSequenceMutableMappingMutableSet

CallableHashable:從圖上可以看出,這兩個抽象基類在標準庫中沒有子類。

numbers包中的抽象基類的繼承關系則很簡單,都是線性的(“數字塔”)。下面5個類從左到右依次派生:

NumberComplexRealRationalIntegral

下面我們將自行定義一個抽象基類并繼承出它的子類。但這并不是鼓勵各位在生產代碼中自定義抽象基類!

3.3 自定義抽象基類

我們將模擬一個隨機抽獎機,它的抽象基類是Tombola,它的4個方法如下:

.load(...):抽象方法,把元素放入容器;

.pick():抽象方法,從容器中隨機返回一個元素,并從容器中刪除該元素;

.loaded():當容器不為空是返回True

.inspect():返回一個有序元組,由容器中的現有元素構成,不修改容器的內容(容器內部元素順序不保留)。

它和它的三個子類的UML圖如下:

以下是Tombola的定義:

# 代碼3.1
import abc

class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self, iterable):
        """從可迭代對象中添加元素"""

    @abc.abstractmethod
    def pick(self):
        """隨機刪除元素,然后將其返回。
        如果實例為空,這個方法應該拋出LookupError,
        這個異常是IndexError和KeyError的基類"""

    def loaded(self):   # 比較耗時,子類可重寫
        """當容器不為空時返回True"""
        return bool(self.inspect())

    def inspect(self):  # 這只是提供一種實現方式,子類可覆蓋該方法
        """返回一個有序元組,由當前元素構成"""
        items = []
        while True:
            try:  # 之所以這么獲取元素,是因為不知道子類如何存儲元素
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))

解釋及補充

導入時,Python并不會檢查抽象方法的實現,在運行時才會真正檢測;

如果子類并沒有實現抽象基類中所有的抽象方法,那么這個子類依然是抽象基類;

抽象方法中可以有實現代碼。即便實現了,子類也必須覆蓋抽象方法,但可以使用super()函數調用抽象方法,為它添加功能,而不是從頭開始寫;

抽象基類中的具體方法只能依賴抽象基類定義的接口

標準庫中有兩個名為abc的模塊,一個是前面說的collections.abc,另一個就是這里的abc模塊。只有在新定義抽象基類的時候才用得到abc.ABC,每個抽象基類都依賴這個類。

abc模塊中本來還有@abstractclassmethod@abstractstaticmethod@abstractproperty三個裝飾器,但這三個從Python3.3起被廢除了,因為這三個的功能都能在@abstractmethod上堆疊其他裝飾器得到,比如實現@abstractclassmethod的功能:

# 代碼3.2
class MyABC(abc.ABC):
    @classmethod
    @abc.abstractmethod
    def an_abstract_classmethod(cls, ...): pass
3.4 定義子類

以下是它的兩個子類的實現代碼:

# # 代碼3.3
class BingoCage(Tombola):  # loaded()和inspect()延用抽象基類的實現
    def __init__(self, items):
        self._randomizer = random.SystemRandom()  # 它會調用os.urandom()
        self._items = []
        self.load(items)   # 委托給load()方法實現初始加載

    def load(self, items):  # 必須實現抽象方法!
        self._items.extend(items)
        self._randomizer.shuffle(self._items)

    def pick(self):  # 必須實現抽象方法!
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError("pick from empty BingoCage")

    def __call__(self):
        self.pick()

class LotteryBlower(Tombola):
    def __init__(self, iterable):
        self._balls = list(iterable)  # 副本

    def load(self, iterable):
        self._balls.extend(iterable)

    def pick(self):
        try:
            position = random.randrange(len(self._balls))
        except ValueError:  # 為了兼容Tombola,并不是拋出ValueError
            raise LookupError("pick from empty LotteryBlower")
        return self._balls.pop(position)

    def loaded(self):  # 覆蓋了抽象基類低效的版本
        return bool(self._balls)

    def inspect(self):
        return tuple(sorted(self._balls))
3.5 虛擬子類

上面兩個子類都是直接繼承自Tombola,而白鵝類型有一個基本特性:即便不用繼承,也能將一個類注冊為抽象基類的虛擬子類。下面是TomboList的實現:

# 代碼3.4
@Tombola.register  # 把TomboList注冊為Tombola的虛擬子類
class TomboList(list):  # 它同時還是list的真實子類,而list其實是MutableSequence的虛擬子類
    def pick(self):
        if self:
            position = random.randrange(len(self))
            return self.pop(position)
        else:
            raise LookupError("pick from empty LotteryBlower")

    load = list.extend  # 當我看到居然這么實現方法時,感覺自己好膚淺......

    def loaded(self):
        return bool(self)

    def inspect(self):
        return tuple(sorted(self))

# Tombola.register(TomboList) 這是register的函數調用版本

下面是這個子類的簡單使用:

# 代碼3.5
>>> issubclass(TomboList, Tombola)
True   # TomboList是Tombola的子類
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True   # TomboList的實例也是Tombola類型
>>> TomboList.__mro__
(, , )
>>> TomboList.__subclasses__()
[, ]

解釋及補充

虛擬子類不會繼承注冊的抽象基類,而且任何時候都不會檢查它是否符合抽象基類的接口,即便在實例化時也不會檢查(如果你的虛擬子類沒有實現抽象方法,在實例化時不會報錯,但如果是繼承而來的話則會報錯),所以為了避免運行時錯誤,虛擬子類應該實現抽象基類的全部方法;

類的繼承關系存儲在一個特殊的類屬性__mro__中,即方法解析順序(Method Resolution Order)。它按順序列出類及其超類,Python則會按照這個順序搜索方法。從上述結果可以看出,這個屬性只存儲了“真實的”超類。

__subclasses__方法返回類的直接子類列表,不含虛擬子類;

雖然現在register可以當做裝飾器用,但更常用的做法還是把它當函數使用。

3.6 另一種虛擬子類

鵝的行為有可能像鴨子。先看如下代碼:

# 代碼3.6
>>> class Struggle:
...     def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True

這里既沒有繼承,也沒有注冊,但Struggle依然被issubclass判斷為abc.Sized的子類。之所以會這樣,是因為abc.Sized實現了一個特殊的類方法__subclasshook__

# # 代碼3.7,abc.Sized的實現在 _collections_abc.py 中
class Sized(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __len__(self):
        return 0

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Sized:
            # 源代碼中是 return _check_methods(C, "__len__"),這里修改了一下
            if any("__len__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

這像不像鴨子類型?只要實現了__len__方法,這個類就是abc.Sized的子類。

在自定義的抽象基類中并不一定要實現__subclasshook__方法,因為即使在Python源碼中,目前也只見到Sized這一個抽象基類實現了__subclasshook__方法,而且Sized只有一個特殊方法。在決定自行實現__subclasshook__方法之前,請想清楚你一定需要這個方法嗎?你的能力能夠保證這個方法的可靠性嗎?

4. 總結

本篇討論的話題只有一個,即“接口”。首先我們討論了鴨子類型的高度動態性,它實現的是動態協議,也是非正式接口;隨后我們借助“白鵝類型”,使用抽象基類明確地、顯示地聲明接口,然后通過子類或注冊來實現這些接口。期間,我們自定義了一個抽象基類,并通過繼承實現了它的兩個子類,還通過注冊實現了它的一個虛擬子類。

最后,還是那句話:不要輕易自定義抽象基類,除非你想構件允許用戶擴展的框架。日常使用中,我們與抽象基類的聯系應該是創建現有抽象基類的子類,或者使用現有的抽象基類注冊。自己從頭編寫新抽象基類的情況非常少。

迎大家關注我的微信公眾號"代碼港" & 個人網站 www.vpointer.net ~

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/41920.html

相關文章

  • Python學習之路31-繼承的利弊

    摘要:使用抽象基類顯示表示接口如果類的作用是定義接口,應該將其明確定義為抽象基類。此外,抽象基類可以作為其他類的唯一基類,混入類則決不能作為唯一的基類,除非這個混入類繼承了另一個更具體的混入這種做法非常少見。 《流暢的Python》筆記本篇是面向對象慣用方法的第五篇,我們將繼續討論繼承,重點說明兩個方面:繼承內置類型時的問題以及多重繼承。概念比較多,較為枯燥。 1. 繼承內置類型 內置類型...

    tinylcy 評論0 收藏0
  • Python基礎之接口——協議抽象基類

    摘要:本文重點協議是中非正式的接口了解抽象基類的基本概念以及標準庫中的抽象基類掌握抽象基類的使用方法。三抽象基類的使用通過繼承聲明抽象基類聲明抽象基類最簡單的方式是繼承或其他抽象基類注意在之間,繼承抽象基類的語法是。 導語:本文章記錄了本人在學習Python基礎之面向對象篇的重點知識及個人心得,打算入門Python的朋友們可以來一起學習并交流。 本文重點: 1、協議是Python中非正式的接...

    TwIStOy 評論0 收藏0
  • 流暢的python讀書筆記-第11章-接口協議抽象基類

    摘要:自己定義的抽象基類要繼承。抽象基類可以包含具體方法。這里想表達的觀點是我們可以偷懶,直接從抽象基類中繼承不是那么理想的具體方法。 抽象基類 抽象基類的常見用途: 實現接口時作為超類使用。 然后,說明抽象基類如何檢查具體子類是否符合接口定義,以及如何使用注冊機制聲明一個類實現了某個接口,而不進行子類化操作。 如何讓抽象基類自動識別任何符合接口的類——不進行子類化或注冊。 接口在動態類...

    Kyxy 評論0 收藏0
  • Python學習之路25-使用一等函數實現設計模式

    摘要:本篇主要講述中使用函數來實現策略模式和命令模式,最后總結出這種做法背后的思想。 《流暢的Python》筆記。本篇主要講述Python中使用函數來實現策略模式和命令模式,最后總結出這種做法背后的思想。 1. 重構策略模式 策略模式如果用面向對象的思想來簡單解釋的話,其實就是多態。父類指向子類,根據子類對同一方法的不同重寫,得到不同結果。 1.1 經典的策略模式 下圖是經典的策略模式的U...

    econi 評論0 收藏0
  • Python學習之路29-序列的修改、散列和切片

    摘要:具體方法和上一篇一樣,也是用各個分量的哈希值進行異或運算,由于的分量可能很多,這里我們使用函數來歸約異或值。每個分量被映射成了它們的哈希值,這些哈希值再歸約成一個值這里的傳入了第三個參數,并且建議最好傳入第三個參數。 《流暢的Python》筆記。本篇是面向對象慣用方法的第三篇。本篇將以上一篇中的Vector2d為基礎,定義多維向量Vector。 1. 前言 自定義Vector類的行為...

    馬忠志 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<