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__()代码。
使用类装饰器方法,不仅能得到相同的结果,还能不涉及多重继承。