Python?類中定義多個(gè)構(gòu)造器方法重載與泛方法
引言
在 Python 開發(fā)中,參數(shù)校驗(yàn)這件事一直讓我很苦惱;校驗(yàn)嘛,就得寫很多與業(yè)務(wù)不相關(guān)的 if
/else
,特別麻煩;不校驗(yàn)嘛,又擔(dān)心會(huì)有哪些意料之外的 bug,而且在很多情況下報(bào)的錯(cuò)誤也和具體問題有些偏差,難以定位問題。
歸功于 Python 動(dòng)態(tài)語言的特性,這個(gè)問題好像沒有很好的解決方式。通過歷代 Python 版本的發(fā)布你也可以看到,官方一直都在完善類型注解(比如最近的 Python 3.11 新增的 Self 類型),似乎想通過某種方式解決此類問題。
不管怎樣,就目前來看,泛方法(generic method)或許能很大程度上解決這個(gè)問題。
什么是“泛方法”?
由多個(gè)方法組成的方法,這些方法為不同的類型實(shí)現(xiàn)相同的操作。
舉個(gè)栗子 ??
現(xiàn)在有個(gè)需求,需要你通過以下幾種方式創(chuàng)建一個(gè)自定義的日期類(CustomDate
):
- 時(shí)間戳
- 年、月、日(包含三個(gè)整數(shù)的元組)
- ISO 格式的字符串
Datetime
類
一般實(shí)現(xiàn)
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
注:這里暫不討論傳入的日期/時(shí)間戳合不合法,僅僅只對(duì)類型做大致判斷。
有沒有更好的方式?
我們可以將不同的構(gòu)建方式拆分為多個(gè)方法,并利用 functools
中的 singledispatchmethod
裝飾器來根據(jù)傳入的參數(shù)類型決定調(diào)用哪個(gè)方法。
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
這樣一來,我們便能將每種參數(shù)類型的初始化獨(dú)立成一個(gè)個(gè)的方法了。
缺點(diǎn)
#1 單分派
在調(diào)用期間應(yīng)該使用哪個(gè)方法實(shí)現(xiàn)由分派算法決定。如果該算法只基于單個(gè)參數(shù)的類型來決定使用哪個(gè)方法實(shí)現(xiàn),則稱其為單分派。
singledispatchmethod
就是就是單分派的。也就是說,只有第一個(gè)參數(shù)會(huì)作為考量。這在實(shí)際業(yè)務(wù)中是遠(yuǎn)遠(yuǎn)不足的。
#2 不支持 typing
然而,如上,對(duì)元組中元素類型判斷還是需要我們用 if
/else
實(shí)現(xiàn)。也就是說,我們不能使用 typing.Tuple[int, int, int]
。
作為一種折中的方案,或許我們可以定義一個(gè) ThreeIntTuple
類來對(duì)其進(jìn)行限定,將這些判斷從 CustomDate
類中隔離開來。
這里僅提供一個(gè)思路讓大家參考,我就不實(shí)現(xiàn)了(因?yàn)槲覀冇懈玫姆绞?xD)。
替代方案:multimethod 庫
這個(gè)庫不是標(biāo)準(zhǔn)庫之一,需要通過 pip 安裝:
pip install multimethod
優(yōu)勢(shì)
multimethod
采用的是多分派算法,能更好地滿足更復(fù)雜的場(chǎng)景。此外,該庫對(duì) typing
中的類型也有不錯(cuò)的支持。
更更好的實(shí)踐方式
回到上面的問題,我們可以這么改進(jìn):
- 使用
multimethod
方法來替代singledispatchmethod
; - 使用
Tuple[int, int, int]
來替代tuple
,不再需要手動(dòng)校驗(yàn)元組的長度和元素類型了;
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
究極好的實(shí)踐方式(真正的方法重載)
在此之前,先問大家一個(gè)簡單的問題(這跟我們之后的內(nèi)容有很大的聯(lián)系):
class A: def a(self): print(1) def a(self): print(2) A().a()
以上這段代碼會(huì)輸出什么?還是會(huì)拋出錯(cuò)誤?
輸出 2
。
在 Python 中,如果定義了重名的方法,最后一個(gè)方法是會(huì)覆蓋掉之前的方法的。
但你或許不知,我們可以通過元類(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 行成功地調(diào)用它了。
multimethod
的維護(hù)者們很好地運(yùn)用了這一點(diǎn),對(duì)重名的方法進(jìn)行了處理,以達(dá)到一種“特殊的效果”。
回到正題,我們可以做出如下改進(jìn):
- 將
multimethod.multidata
設(shè)置為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
從效果上來看,這完全和靜態(tài)語言的方法重載一模一樣!
總結(jié)
使用泛方法能讓代碼更加簡潔、靈活。希望上述內(nèi)容能對(duì)你有幫助。
參考資料
以上就是Python 類中定義多個(gè)構(gòu)造器方法重載與泛方法的詳細(xì)內(nèi)容,更多關(guān)于Python 類多個(gè)構(gòu)造器方法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用Pyinstaller轉(zhuǎn)換.py文件為.exe可執(zhí)行程序過程詳解
這篇文章主要介紹了使用Pyinstaller轉(zhuǎn)換.py文件為.exe可執(zhí)行程序過程詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08利用Python實(shí)現(xiàn)簡易計(jì)算器的示例代碼
最近學(xué)習(xí)了字符串,運(yùn)算符,條件語句,循環(huán)語句,我在想可以用我最近學(xué)的東西做什么? 看到運(yùn)算我就想到了可以做一個(gè)簡易的計(jì)算器,感興趣的可以了解一下2022-11-11Python數(shù)據(jù)分析之雙色球中藍(lán)紅球分析統(tǒng)計(jì)示例
這篇文章主要介紹了Python數(shù)據(jù)分析之雙色球中藍(lán)紅球分析統(tǒng)計(jì),結(jié)合實(shí)例形式較為詳細(xì)的分析了Python針對(duì)雙色球藍(lán)紅球中獎(jiǎng)數(shù)據(jù)分析的相關(guān)操作技巧,需要的朋友可以參考下2018-02-02python中exec函數(shù)的實(shí)現(xiàn)
exec()是Python內(nèi)置的一個(gè)函數(shù),用于在運(yùn)行時(shí)執(zhí)行動(dòng)態(tài)生成的Python代碼,下面就來介紹一下python中exec函數(shù)的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10Python實(shí)現(xiàn)softmax反向傳播的示例代碼
這篇文章主要為大家詳細(xì)介紹了Python實(shí)現(xiàn)softmax反向傳播的相關(guān)資料,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的可以了解一下2023-04-04Python?隊(duì)列Queue和PriorityQueue解析
這篇文章主要介紹了Python?隊(duì)列Queue和PriorityQueue,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09