Python類別中如何定義多個建構器方法重載和泛方法

WBOY
發布: 2023-05-09 14:34:23
轉載
1292 人瀏覽過

    什麼是「泛方法」?

    由多個方法組成的方法,這些方法為不同的類型實現相同的操作。

    舉個栗子 

    現在有個需求,需要你透過以下幾種方式建立一個自訂的日期類別(CustomDate):

    • #時間戳記

    • 年、月、日(包含三個整數的元組)

    • ISO 格式的字符字串

    • Datetime 類別

    #一般實作

    from datetime import date, datetime
    class CustomDate:
        def __init__(self, arg):
            if isinstance(arg, (int, float)):
                self.__date = date.fromtimestamp(arg)
            elif isinstance(arg, tuple) and len(arg) == 3 and all(map(lambda x: isinstance(x, int), arg):
                self.__date = date(*arg)
            elif isinstance(arg, str):
                self.__date = date.fromisoformat(arg)
            elif isinstance(arg, datetime):
                self.__date = datetime.date()
            else:
                raise TypeError("could not create instance from " + type(arg).__name__)
        @property
        def date():
            return self.__date
    登入後複製

    註:這裡暫不討論傳入的日期/時間戳記合不合法,僅對類型做大致判斷。

    有沒有更好的方式?

    我們可以將不同的建置方式拆分為多個方法,並利用functools 中的singledispatchmethod 裝飾器來根據傳入的參數類型決定呼叫哪一個方法。

    from datetime import date, datetime
    from functools import singledispatchmethod
    class CustomDate:
        @singledispatchmethod
        def __init__(self, arg):
            raise TypeError("could not create instance from " + type(arg).__name__)
        @__init__.register(int)
        @__init__.register(float)
        def __from_timestamp(self, arg):
            self.__date = date.fromtimestamp(arg)
        @__init__.register(tuple)
        def __from_tuple(self, arg):
            if len(arg) == 3 and all(map(lambda x: isinstance(x, int), arg)):
                self.__date = date(*arg)
            else:
                raise ValueError("could not create instance from a malformed tuple")
        @__init__.register(str)
        def __from_isoformat(self, arg):
            self.__date = date.fromisoformat(arg)
        @__init__.register(datetime)
        def __from_datetime(self, arg):
            self.__date = arg.date()
        @property
        def date(self):
            return self.__date
    登入後複製

    這樣一來,我們便能將每種參數類型的初始化獨立成一個個的方法了。

    缺點

    #1 單分派

    在呼叫期間應該使用哪個方法實作由分派演算法決定。如果演算法只基於單一參數的類型來決定使用哪個方法實現,則稱其為單分派。

    singledispatchmethod 就是就是單分派的。也就是說,只有第一個參數會作為考量。這在實際業務中是遠遠不足的。

    #2 不支援 typing

    然而,如上,對元組中元素類型判斷還是需要我們用 if/else 實作。也就是說,我們不能使用 typing.Tuple[int, int, int]

    作為一種折中的方案,或許我們可以定義一個 ThreeIntTuple 類別來對其進行限定,將這些判斷從 CustomDate 類別中隔離開來。

    這裡只提供一個想法讓大家參考,我就不實現了(因為我們有更好的方式 xD)。

    替代方案:multimethod 函式庫

    這個函式庫不是標準函式庫之一,需要透過pip 安裝:

    pip install multimethod
    登入後複製

    優勢

    multimethod 採用的是多分派演算法,能更好地滿足更複雜的場景。此外,該程式庫對 typing 中的類型也有不錯的支援。

    更更好的實踐方式

    回到上面的問題,我們可以這麼改進:

    • 使用multimethod 方法來替代singledispatchmethod;

    • 使用Tuple[int, int, int] 來取代tuple,不再需要手動校驗元組的長度和元素類型了;

    from datetime import date, datetime
    from typing import Tuple, Union
    from multimethod import multimethod
    class CustomDate:
        @multimethod
        def __init__(self, arg):
            raise TypeError("could not create instance from " + type(arg).__name__)
        @__init__.register
        def __from_timestamp(self, arg: Union[int, float]):
            self.__date = date.fromtimestamp(arg)
        @__init__.register
        def __from_tuple(self, arg: Tuple[int, int, int]):
            self.__date = date(*arg)
        @__init__.register
        def __from_isoformat(self, arg: str):
            self.__date = date.fromisoformat(arg)
        @__init__.register
        def __from_datetime(self, arg: datetime):
            self.__date = arg.date()
        @property
        def date(self):
            return self.__date
    登入後複製

    究極好的實踐方式(真正的方法重載)

    在此之前,先問大家一個簡單的問題(這跟我們之後的內容有很大的連結):

    class A:
        def a(self):
            print(1)
        def a(self):
            print(2)
    A().a()
    登入後複製

    以上這段程式碼會輸出什麼?還是會拋出錯誤?

    輸出 2

    在 Python 中,如果定義了重名的方法,最後一個方法是會覆寫之前的方法的。

    但你或許不知,我們可以透過元類別(metaclass)來改變這個行為:

    class MetaA(type):
        class __prepare__(dict):
            def __init__(*args):
                pass
            def __setitem__(self, key, value):
                if self.get('a'):  # Line 7
                    super().__setitem__('b', value)  # Line 8
                else:	
                    super().__setitem__(key, value)
    class A(metaclass=MetaA):
        def a(self):
            print(1)
        def a(self):
            print(2)
    A().a()  # => 1
    A().b()  # => 2  # Line 22
    登入後複製

    在第7 和第8 行,我們將重名的a 方法改名為b,並在第22 行成功地呼叫它了。

    multimethod 的維護者們很好地運用了這一點,對重名的方法進行了處理,以達到一種「特殊的效果」。

    回到正題,我們可以做出以下改進:

    • multimethod.multidata 設定為CustomDate 類別的元類別;

    • 將所有方法命名為__init__

    from datetime import date, datetime
    from typing import Tuple, Union
    from multimethod import multimeta
    class CustomDate(metaclass=multimeta):
        def __init__(self, arg: Union[int, float]):
            self.__date = date.fromtimestamp(arg)
        def __init__(self, arg: Tuple[int, int, int]):
            self.__date = date(*arg)
        def __init__(self, arg: str):
            self.__date = date.fromisoformat(arg)
        def __init__(self, arg: datetime):
            self.__date = arg.date()
        def __init__(self, arg):
            raise TypeError("could not create instance from " + type(arg).__name__)
        @property
        def date(self):
            return self.__date
    登入後複製

    從效果來看,這完全和靜態語言的方法重載一模一樣!

    以上是Python類別中如何定義多個建構器方法重載和泛方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    相關標籤:
    來源:yisu.com
    本網站聲明
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
    熱門教學
    更多>
    最新下載
    更多>
    網站特效
    網站源碼
    網站素材
    前端模板
    關於我們 免責聲明 Sitemap
    PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!