使用Mixin技術拓展類定義,實現python類功能的可插拔
Q:我們有一些十分有用的方法,希望用它來拓展其他類的方法,但是需要新增方法的這些類之間並不一定屬於繼承關係。因此,沒有辦法將這些方法直接關聯到一個共同的基類上。
為了解決這個問題,我們可以使用Mixin技術,有兩個實現方法,一個是多重繼承,一個是類裝飾器。
首先展示多重繼承的方式。我們定義了一些定製化的處理方法,比如日誌記錄和型別檢查等,我們希望將這些新增到物件中。
class LoggedMixin: """ 當獲取/設定/清除 屬性時,列印日誌 """ __slot__ = () def __getitem__(self, key): print('Getting '+ str(key)) return super().__getitem__(key) def __setitem__(self, key, value): print('Setting {} = {!r}'.format(key, value)) return super().__setitem__(key, value) def __delitem__(self, key): print('Deleting ' + str(key)) return super().__delitem__(key) class SetOnceMixin: """ 用來控制一個屬性只能被設定一次 """ __slot__ = () def __setitem__(self, key, value): if key in self: raise KeyError(str(key) +' already set') return super().__setitem__(key, value) class StringKeysMixin: """ 用來控制鍵值只能是字串 """ __slot__ = () def __setitem__(self, key, value): if not isinstance(key, str): raise TypeError('keys must be strings') return super().__setitem__(key,value)
這些類本身是沒有用的。實際上,如果例項化它們中的任何一個,除了產生異常外,一點左右都沒有。這些類存在的意義就是和其他類通過多重繼承的方式混合在一起使用。
示例如下:
>>> class StringDict(StringKeysMixin, SetOnceMixin, LoggedMixin, dict): pass >>> d = StringDict() >>> d[1] = 1 #限定只能使用字串作為鍵值 Traceback (most recent call last): File "<pyshell#14>", line 1, in <module> d[1] = 1 File "C:\Users\Administrator\Desktop\1044.py", line 38, in __setitem__ raise TypeError('keys must be strings') TypeError: keys must be strings >>> d['1'] = 1 #列印設定值的日誌 Setting 1 = 1 >>> d['1'] = 1 #限定每個鍵只能被設定一次 Traceback (most recent call last): File "<pyshell#16>", line 1, in <module> d['1'] = 1 File "C:\Users\Administrator\Desktop\1044.py", line 39, in __setitem__ return super().__setitem__(key,value) File "C:\Users\Administrator\Desktop\1044.py", line 27, in __setitem__ raise KeyError(str(key) +' already set') KeyError: '1 already set' >>> d #同時擁有普通字典的所有功能 {'1': 1}
是不是很神奇?通過Mixin的技術,實現了類的功能可插拔,我們可以使用這樣的技術為自己來定義一個定製化的類。
在mixin類中,呼叫super()函式是必要的,這也是編寫mixin類的關鍵部分。在程式碼中,這些類重新定義了一些特定的關鍵方法,比如__getitem__()和__setitem__()方法。但是,他們也需要呼叫這些方法的原始版本。通過使用super(),將這個任務轉交給了方法解析順序(MRO)上的下一個類。也許在父類中定義super()看起來好像是錯誤的,但是在StringDict類的實現中,所有操作最後都會通過super()函式把任務轉交給多重繼承列表的下一個類。即最終呼叫的是dict的方法,例如dict.__setitem__()等。如果沒有這樣的行為,mixin根本沒有辦法工作。
但是要注意的是,Mixin類絕不是為了直接例項化而建立的,他們必須和另一個實現了所需的對映功能的類混合在一起用才行。
其次,Mixin類沒有__init__()方法,也沒有例項變數,我們定義的__slots__ = ()就是一種強烈暗示,這表示mixin類沒有屬於自己的例項資料。
但是如果考慮一個擁有__init__()方法以及例項變數的mixin類呢?這會帶來極大的風險,因為這個類並不知道自己要和哪些其他類混合在一起。任何創建出來的例項變數都必須以某種方式加以命名,以避免出現命名衝突。此外,mixin類的__init__()方法也必須要能合適的呼叫其他混進來的類的__init__()方法,一般來說這是很難實現的,因為不知道其他類的引數簽名是什麼,至少我們必須得實現非常通用的引數簽名,這需要使用到*args和**kwargs。而如果mixin類自身的__init__()方法還帶了引數,那這些引數應該只能通過關鍵字來指明,並且在名稱空間上還得和其他引數區分開來。
對於一個定義了__init__()方法並接受一個關鍵字引數的mixin類,下面給出一種可能的實現方法:
class RestricKeyMixin:
def __init__(self, *args, _restrict_key_type, **kwargs):
self.__restrict_key_type = _restrict_key_type
super().__init__(*args, **kwargs)
def __setitem__(self, key, value):
if not isinstance(key, self.__restrict_key_type):
raise TypeError('Keys must be '+ str(self.__restrict_key_type))
super().__setitem__(key, value)
>>> class RDict(RestricKeyMixin, dict):
pass
>>> d = RDict(_restrict_key_type = str, name = 'Amos')
>>> d
{'name': 'Amos'}
>>> d[10] = 1998
Traceback (most recent call last):
File "<pyshell#26>", line 1, in <module>
d[10] = 1998
File "C:\Users\Administrator\Desktop\1044.py", line 8, in __setitem__
raise TypeError('Keys must be '+ str(self.__restrict_key_type))
TypeError: Keys must be <class 'str'>
在這個例子中,初始化RDict時仍然帶有可以被dict()所接受的引數,但是必須有一個額外的關鍵字引數_restrict_key_type 提供給mixin類。
最後,實現mixin的另一種方法是利用類裝飾器。考慮如下程式碼:
def LoggedMixin(cls):
cls_getitem = cls.__getitem__
cls_setitem = cls.__setitem__
cls_delitem = cls.__delitem__
def __getitem__(self, key):
print('Getting '+ str(key))
return cls_getitem(self, key)
def __setitem__(self, key, value):
print('Setting {} = {!r}'.format(key, value))
return cls_setitem(self, key, value)
def __delitem__(self, key):
print('Deleting ' + str(key))
return cls_delitem__(self.key)
cls.__getitem__ = __getitem__
cls.__setitem__ = __setitem__
cls.__delitem__ = __delitem__
return cls
>>> @LoggedMixin
class LoggedDict(dict):
pass
>>> d = LoggedDict()
>>> d[1] = 1
Setting 1 = 1
使用這種裝飾器方法,首先使用cls_xxxitem變數儲存了未修改的cls.__xxxitem__方法
之後自定義cls.__xxxitem__方法,在例項中進行屬性操作的時候就會自動呼叫修改後的__xxxitem__()方法,執行完自定義程式碼後,在執行原生的__xxxitem__()程式碼。
使用類裝飾器方法,不僅能得到相同的結果,還能不涉及多重繼承。